Merge branch 'master' into mm-1391
Этот коммит содержится в:
68
README.md
68
README.md
@@ -10,13 +10,21 @@ Mattermost is a team communication service. It brings team messaging and file sh
|
||||
|
||||
We built Mattermost to help teams focus on what matters most to them. It works for us, we hope it works for you too.
|
||||
|
||||
Learn More
|
||||
==========
|
||||
<ul>
|
||||
<li/>Ask the core team anything at: http://forum.mattermost.org</li>
|
||||
<li/>Share feature requests and upvotes: http://www.mattermost.org/feature-requests/</li>
|
||||
<li/>File bugs: http://www.mattermost.org/filing-issues/</li>
|
||||
<li/>Make a pull request: http://www.mattermost.org/contribute-to-mattermost/</li>
|
||||
</ul>
|
||||
|
||||
Installing the Mattermost
|
||||
=========================
|
||||
|
||||
You're installing "Mattermost Preview", a pre-released 0.50 version intended for an early look at what we're building. While SpinPunch runs this version internally, it's not recommended for production deployments since we can't guarantee API stability or backwards compatibility until our 1.0 version release.
|
||||
|
||||
That said, any issues at all, please let us know on the Mattermost forum at: http://bit.ly/1MY1kul
|
||||
That said, any issues at all, please let us know on the Mattermost forum at: http://forum.mattermost.org
|
||||
|
||||
Local Machine Setup (Docker)
|
||||
-----------------------------
|
||||
@@ -33,38 +41,38 @@ Local Machine Setup (Docker)
|
||||
6. When docker is done fetching the image, open http://dockerhost:8065/ in your browser
|
||||
|
||||
### Ubuntu ###
|
||||
1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summery below.
|
||||
1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summary below.
|
||||
|
||||
`sudo apt-get update`
|
||||
``` bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install wget
|
||||
wget -qO- https://get.docker.com/ | sh
|
||||
sudo usermod -aG docker <username>
|
||||
sudo service docker start
|
||||
newgrp docker
|
||||
```
|
||||
|
||||
`sudo apt-get install wget`
|
||||
|
||||
`wget -qO- https://get.docker.com/ | sh`
|
||||
|
||||
`sudo usermod -aG docker <username>`
|
||||
|
||||
`sudo service docker start`
|
||||
|
||||
`newgrp docker`
|
||||
|
||||
2. Run `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium
|
||||
2. Run `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium`
|
||||
3. When docker is done fetching the image, open http://localhost:8065/ in your browser
|
||||
|
||||
### Arch ###
|
||||
1. Install docker using the following commands
|
||||
1. Install docker using the following commands:
|
||||
|
||||
`pacman -S docker`
|
||||
``` bash
|
||||
pacman -S docker
|
||||
systemctl enable docker.service
|
||||
systemctl start docker.service
|
||||
gpasswd -a <username> docker
|
||||
newgrp docker
|
||||
```
|
||||
|
||||
`systemctl enable docker.service`
|
||||
2. Start docker container:
|
||||
|
||||
`systemctl start docker.service`
|
||||
``` bash
|
||||
docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium
|
||||
```
|
||||
|
||||
`gpasswd -a <username> docker`
|
||||
|
||||
`newgrp docker`
|
||||
|
||||
2. docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium
|
||||
3. When docker is done fetching the image, open http://localhost:8065/ in your browser
|
||||
3. When docker is done fetching the image, open http://localhost:8065/ in your browser.
|
||||
|
||||
### Notes ###
|
||||
If your ISP blocks port 25 then you may install locally but email will not be sent.
|
||||
@@ -80,6 +88,8 @@ If you wish to remove mattermost-dev use the following commands
|
||||
1. `docker stop mattermost-dev`
|
||||
2. `docker rm -v mattermost-dev`
|
||||
|
||||
If you wish to gain access to the container use the following commands
|
||||
1. `docker exec -ti mattermost-dev /bin/bash`
|
||||
|
||||
AWS Elastic Beanstalk Setup (Docker)
|
||||
------------------------------------
|
||||
@@ -89,7 +99,7 @@ AWS Elastic Beanstalk Setup (Docker)
|
||||
2. Select "Create New Application" from the top right.
|
||||
3. Name the application and press next
|
||||
4. Select "Create a web server" environment.
|
||||
5. If asked, select create and AIM role and instance profile and press next.
|
||||
5. If asked, select create and IAM role and instance profile and press next.
|
||||
6. For predefined configuration select docker. For environment type select single instance.
|
||||
7. For application source, select upload your own and upload Dockerrun.aws.json from docker/Dockerrun.aws.json. Everything else may be left at default.
|
||||
8. Select an environment name, this is how you will refer to your environment. Make sure the URL is available then press next.
|
||||
@@ -107,7 +117,7 @@ AWS Elastic Beanstalk Setup (Docker)
|
||||
18. Modify an existing CNAME record set or create a new one with the name * and the value of the domain you copied in step 1.13.
|
||||
19. Save the record set
|
||||
|
||||
3. Set the enviroment variable "MATTERMOST\_DOMAIN" to the domain you mapped above (example.com not www.example.com)
|
||||
3. Set the environment variable "MATTERMOST\_DOMAIN" to the domain you mapped above (example.com not www.example.com)
|
||||
20. Return the Elastic Beanstalk from the AWS console.
|
||||
21. Select the environment you created.
|
||||
22. Select configuration from the sidebar.
|
||||
@@ -119,6 +129,11 @@ AWS Elastic Beanstalk Setup (Docker)
|
||||
26. Return to the dashboard on the sidebar and wait for beanstalk update the environment.
|
||||
27. Try it out by entering the domain you mapped into your browser.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
To contribute to this open source project please review the Mattermost Contribution Guidelines at http://www.mattermost.org/contribute-to-mattermost/.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
@@ -126,3 +141,4 @@ Most Mattermost source files are made available under the terms of the GNU Affer
|
||||
|
||||
As an exception, Admin Tools and Configuration Files are are made available under the terms of the Apache License, version 2.0. See LICENSE.txt for more information.
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if sc, err := CreateChannel(c, channel, r.URL.Path, true); err != nil {
|
||||
if sc, err := CreateChannel(c, channel, true); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
@@ -65,7 +65,7 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func CreateChannel(c *Context, channel *model.Channel, path string, addMember bool) (*model.Channel, *model.AppError) {
|
||||
func CreateChannel(c *Context, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
|
||||
if result := <-Srv.Store.Channel().Save(channel); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
@@ -100,7 +100,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if sc, err := CreateDirectChannel(c, userId, r.URL.Path); err != nil {
|
||||
if sc, err := CreateDirectChannel(c, userId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
@@ -108,7 +108,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Channel, *model.AppError) {
|
||||
func CreateDirectChannel(c *Context, otherUserId string) (*model.Channel, *model.AppError) {
|
||||
if len(otherUserId) != 26 {
|
||||
return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId)
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Ch
|
||||
return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId)
|
||||
}
|
||||
|
||||
if sc, err := CreateChannel(c, channel, path, true); err != nil {
|
||||
if sc, err := CreateChannel(c, channel, true); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId,
|
||||
@@ -146,6 +146,23 @@ func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Ch
|
||||
}
|
||||
}
|
||||
|
||||
func CreateDefaultChannels(c *Context, teamId string) ([]*model.Channel, *model.AppError) {
|
||||
townSquare := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: teamId}
|
||||
|
||||
if _, err := CreateChannel(c, townSquare, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offTopic := &model.Channel{DisplayName: "Off-Topic", Name: "off-topic", Type: model.CHANNEL_OPEN, TeamId: teamId}
|
||||
|
||||
if _, err := CreateChannel(c, offTopic, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels := []*model.Channel{townSquare, offTopic}
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
channel := model.ChannelFromJson(r.Body)
|
||||
@@ -303,7 +320,7 @@ func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
channelId := params["id"]
|
||||
|
||||
JoinChannel(c, channelId, r.URL.Path)
|
||||
JoinChannel(c, channelId, "")
|
||||
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -314,7 +331,7 @@ func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(model.MapToJson(result)))
|
||||
}
|
||||
|
||||
func JoinChannel(c *Context, channelId string, path string) {
|
||||
func JoinChannel(c *Context, channelId string, role string) {
|
||||
|
||||
sc := Srv.Store.Channel().Get(channelId)
|
||||
uc := Srv.Store.User().Get(c.Session.UserId)
|
||||
@@ -340,7 +357,7 @@ func JoinChannel(c *Context, channelId string, path string) {
|
||||
}
|
||||
|
||||
if channel.Type == model.CHANNEL_OPEN {
|
||||
cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId, NotifyLevel: model.CHANNEL_NOTIFY_ALL}
|
||||
cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: role}
|
||||
|
||||
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
|
||||
c.Err = cmresult.Err
|
||||
@@ -363,6 +380,32 @@ func JoinChannel(c *Context, channelId string, path string) {
|
||||
}
|
||||
}
|
||||
|
||||
func JoinDefaultChannels(c *Context, user *model.User, channelRole string) *model.AppError {
|
||||
// We don't call JoinChannel here since c.Session is not populated on user creation
|
||||
|
||||
var err *model.AppError = nil
|
||||
|
||||
if result := <-Srv.Store.Channel().GetByName(user.TeamId, "town-square"); result.Err != nil {
|
||||
err = result.Err
|
||||
} else {
|
||||
cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
|
||||
if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
|
||||
err = cmResult.Err
|
||||
}
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Channel().GetByName(user.TeamId, "off-topic"); result.Err != nil {
|
||||
err = result.Err
|
||||
} else {
|
||||
cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
|
||||
if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
|
||||
err = cmResult.Err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func leaveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
@@ -35,8 +35,15 @@ func TestCreateChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
rget := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
|
||||
if rget.Channels[0].Name != channel.Name {
|
||||
t.Fatal("full name didn't match")
|
||||
nameMatch := false
|
||||
for _, c := range rget.Channels {
|
||||
if c.Name == channel.Name {
|
||||
nameMatch = true
|
||||
}
|
||||
}
|
||||
|
||||
if !nameMatch {
|
||||
t.Fatal("Did not create channel with correct name")
|
||||
}
|
||||
|
||||
if _, err := Client.CreateChannel(rchannel.Data.(*model.Channel)); err == nil {
|
||||
@@ -679,6 +686,8 @@ func TestUpdateNotifyLevel(t *testing.T) {
|
||||
data["user_id"] = user.Id
|
||||
data["notify_level"] = model.CHANNEL_NOTIFY_MENTION
|
||||
|
||||
timeBeforeUpdate := model.GetMillis()
|
||||
|
||||
if _, err := Client.UpdateNotifyLevel(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -689,6 +698,10 @@ func TestUpdateNotifyLevel(t *testing.T) {
|
||||
t.Fatal("NotifyLevel did not update properly")
|
||||
}
|
||||
|
||||
if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
|
||||
t.Fatal("LastUpdateAt did not update")
|
||||
}
|
||||
|
||||
data["user_id"] = "junk"
|
||||
if _, err := Client.UpdateNotifyLevel(data); err == nil {
|
||||
t.Fatal("Should have errored - bad user id")
|
||||
@@ -735,7 +748,7 @@ func TestUpdateNotifyLevel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFuzzyChannel(t *testing.T) {
|
||||
Setup();
|
||||
Setup()
|
||||
|
||||
team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
@@ -747,9 +760,9 @@ func TestFuzzyChannel(t *testing.T) {
|
||||
Client.LoginByEmail(team.Domain, user.Email, "pwd")
|
||||
|
||||
// Strings that should pass as acceptable channel names
|
||||
var fuzzyStringsPass = []string {
|
||||
var fuzzyStringsPass = []string{
|
||||
"*", "?", ".", "}{][)(><", "{}[]()<>",
|
||||
|
||||
|
||||
"qahwah ( قهوة)",
|
||||
"שָׁלוֹם עֲלֵיכֶם",
|
||||
"Ramen チャーシュー chāshū",
|
||||
|
||||
@@ -197,7 +197,7 @@ func joinCommand(c *Context, command *model.Command) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
JoinChannel(c, v.Id, "/command")
|
||||
JoinChannel(c, v.Id, "")
|
||||
|
||||
if c.Err != nil {
|
||||
return false
|
||||
|
||||
@@ -129,7 +129,7 @@ func TestJoinCommands(t *testing.T) {
|
||||
|
||||
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
|
||||
|
||||
if len(c1.Channels) != 3 { // 3 because of town-square and direct
|
||||
if len(c1.Channels) != 4 { // 4 because of town-square, off-topic and direct
|
||||
t.Fatal("didn't join channel")
|
||||
}
|
||||
|
||||
|
||||
16
api/post.go
16
api/post.go
@@ -227,7 +227,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
|
||||
channel = result.Data.(*model.Channel)
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
bodyText = "You have one new message."
|
||||
subjectText = "New Direct Message"
|
||||
subjectText = "New Private Message"
|
||||
} else {
|
||||
bodyText = "You have one new mention."
|
||||
subjectText = "New Mention"
|
||||
@@ -273,7 +273,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
|
||||
|
||||
} else {
|
||||
|
||||
// Find out who is a member of the channel only keep those profiles
|
||||
// Find out who is a member of the channel, only keep those profiles
|
||||
if eResult := <-echan; eResult.Err != nil {
|
||||
l4g.Error("Failed to get channel members channel_id=%v err=%v", post.ChannelId, eResult.Err.Message)
|
||||
return
|
||||
@@ -306,13 +306,23 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add @all to keywords if user has them turned on
|
||||
if profile.NotifyProps["all"] == "true" {
|
||||
keywordMap["@all"] = append(keywordMap["@all"], profile.Id)
|
||||
}
|
||||
|
||||
// Add @channel to keywords if user has them turned on
|
||||
if profile.NotifyProps["channel"] == "true" {
|
||||
keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// Build a map as a list of unique user_ids that are mentioned in this post
|
||||
splitF := func(c rune) bool {
|
||||
return model.SplitRunes[c]
|
||||
}
|
||||
splitMessage := strings.FieldsFunc(strings.Replace(post.Message, "<br>", " ", -1), splitF)
|
||||
splitMessage := strings.FieldsFunc(post.Message, splitF)
|
||||
for _, word := range splitMessage {
|
||||
|
||||
// Non-case-sensitive check for regular keys
|
||||
|
||||
21
api/team.go
21
api/team.go
@@ -146,10 +146,8 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
rteam := result.Data.(*model.Team)
|
||||
|
||||
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id}
|
||||
|
||||
if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil {
|
||||
c.Err = err
|
||||
if _, err := CreateDefaultChannels(c, rteam.Id); err != nil {
|
||||
c.Err = nil
|
||||
return
|
||||
}
|
||||
|
||||
@@ -197,10 +195,8 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
rteam := result.Data.(*model.Team)
|
||||
|
||||
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id}
|
||||
|
||||
if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil {
|
||||
c.Err = err
|
||||
if _, err := CreateDefaultChannels(c, rteam.Id); err != nil {
|
||||
c.Err = nil
|
||||
return
|
||||
}
|
||||
|
||||
@@ -488,12 +484,21 @@ func InviteMembers(team *model.Team, user *model.User, invites []string) {
|
||||
} else {
|
||||
sender = user.FullName
|
||||
}
|
||||
|
||||
senderRole := ""
|
||||
if strings.Contains(user.Roles, model.ROLE_ADMIN) || strings.Contains(user.Roles, model.ROLE_SYSTEM_ADMIN) {
|
||||
senderRole = "administrator"
|
||||
} else {
|
||||
senderRole = "member"
|
||||
}
|
||||
|
||||
subjectPage := NewServerTemplatePage("invite_subject", teamUrl)
|
||||
subjectPage.Props["SenderName"] = sender
|
||||
subjectPage.Props["TeamName"] = team.Name
|
||||
bodyPage := NewServerTemplatePage("invite_body", teamUrl)
|
||||
bodyPage.Props["TeamName"] = team.Name
|
||||
bodyPage.Props["SenderName"] = sender
|
||||
bodyPage.Props["SenderStatus"] = senderRole
|
||||
|
||||
bodyPage.Props["Email"] = invite
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@ func TestCreateFromSignupTeam(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
|
||||
if len(c1.Channels) != 2 {
|
||||
t.Fatal("default channels not created")
|
||||
}
|
||||
|
||||
ts.Data = "garbage"
|
||||
_, err = Client.CreateTeamFromSignup(&ts)
|
||||
if err == nil {
|
||||
@@ -71,6 +76,17 @@ func TestCreateTeam(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user := &model.User{TeamId: rteam.Data.(*model.Team).Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
Srv.Store.User().VerifyEmail(user.Id)
|
||||
|
||||
Client.LoginByEmail(team.Domain, user.Email, "pwd")
|
||||
|
||||
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
|
||||
if len(c1.Channels) != 2 {
|
||||
t.Fatal("default channels not created")
|
||||
}
|
||||
|
||||
if rteam.Data.(*model.Team).Name != team.Name {
|
||||
t.Fatal("full name didn't match")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
|
||||
<link href='https://fonts.googleapis.com/css?family=Lato:400,700,900' rel='stylesheet' type='text/css'>
|
||||
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,600,700' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" href="/static/css/styles.css">
|
||||
</head>
|
||||
<body class="white error">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
|
||||
<h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2>
|
||||
<p>{{.Props.TeamName}} started using {{.SiteName}}.<br> The team administrator <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamName}}</strong>.</p>
|
||||
<p>{{.Props.TeamName}} started using {{.SiteName}}.<br> The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamName}}</strong>.</p>
|
||||
<p style="margin: 20px 0 15px">
|
||||
<a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Join Team</a>
|
||||
</p>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<h2 style="font-weight: normal; margin-top: 10px;">You were mentioned</h2>
|
||||
<p>CHANNEL: {{.Props.ChannelName}}<br>{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} GMT, {{.Props.Month}} {{.Props.Day}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif;">{{.Props.PostMessage}}</pre></p>
|
||||
<p style="margin: 20px 0 15px">
|
||||
<a href="{{.Props.TeamLink}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a>
|
||||
<a href="{{.Props.TeamLink}}" style="background: #2389D7; display: inline-block; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 170px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
15
api/user.go
15
api/user.go
@@ -176,21 +176,16 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
|
||||
} else {
|
||||
ruser := result.Data.(*model.User)
|
||||
|
||||
// Do not error if user cannot be added to the town-square channel
|
||||
if cresult := <-Srv.Store.Channel().GetByName(team.Id, "town-square"); cresult.Err != nil {
|
||||
l4g.Error("Failed to get town-square err=%v", cresult.Err)
|
||||
} else {
|
||||
cm := &model.ChannelMember{ChannelId: cresult.Data.(*model.Channel).Id, UserId: ruser.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
|
||||
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
|
||||
l4g.Error("Failed to add member town-square err=%v", cmresult.Err)
|
||||
}
|
||||
// Soft error if there is an issue joining the default channels
|
||||
if err := JoinDefaultChannels(c, ruser, channelRole); err != nil {
|
||||
l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err)
|
||||
}
|
||||
|
||||
//fireAndForgetWelcomeEmail(strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl+"/channels/town-square")
|
||||
|
||||
if user.EmailVerified {
|
||||
if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
|
||||
l4g.Error("Failed to get town-square err=%v", cresult.Err)
|
||||
l4g.Error("Failed to set email verified err=%v", cresult.Err)
|
||||
}
|
||||
} else {
|
||||
FireAndForgetVerifyEmail(result.Data.(*model.User).Id, strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl)
|
||||
@@ -198,7 +193,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
|
||||
|
||||
ruser.Sanitize(map[string]bool{})
|
||||
|
||||
//This message goes to every channel, so the channelId is irrelevant
|
||||
// This message goes to every channel, so the channelId is irrelevant
|
||||
message := model.NewMessage(team.Id, "", ruser.Id, model.ACTION_NEW_USER)
|
||||
|
||||
store.PublishAndForget(message)
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestSocket(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestZZWebScoketTearDown(t *testing.T) {
|
||||
func TestZZWebSocketTearDown(t *testing.T) {
|
||||
// *IMPORTANT* - Kind of hacky
|
||||
// This should be the last function in any test file
|
||||
// that calls Setup()
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"ServiceSettings": {
|
||||
"SiteName": "Mattermost",
|
||||
"Domain": "",
|
||||
"Mode" : "prod",
|
||||
"Mode" : "dev",
|
||||
"AllowTesting" : false,
|
||||
"UseSSL": false,
|
||||
"Port": "80",
|
||||
|
||||
@@ -78,7 +78,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
||||
createdTeam := result.Data.(*model.Team)
|
||||
|
||||
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: createdTeam.Id}
|
||||
if _, err := api.CreateChannel(c, channel, r.URL.Path, false); err != nil {
|
||||
if _, err := api.CreateChannel(c, channel, false); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ func (o *ChannelList) Etag() string {
|
||||
t = member.LastViewedAt
|
||||
id = v.Id
|
||||
}
|
||||
|
||||
if member.LastUpdateAt > t {
|
||||
t = member.LastUpdateAt
|
||||
id = v.Id
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ type ChannelMember struct {
|
||||
MsgCount int64 `json:"msg_count"`
|
||||
MentionCount int64 `json:"mention_count"`
|
||||
NotifyLevel string `json:"notify_level"`
|
||||
LastUpdateAt int64 `json:"last_update_at"`
|
||||
}
|
||||
|
||||
func (o *ChannelMember) ToJson() string {
|
||||
@@ -70,6 +71,10 @@ func (o *ChannelMember) IsValid() *AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *ChannelMember) PreSave() {
|
||||
o.LastUpdateAt = GetMillis()
|
||||
}
|
||||
|
||||
func IsChannelNotifyLevelValid(notifyLevel string) bool {
|
||||
return notifyLevel == CHANNEL_NOTIFY_ALL || notifyLevel == CHANNEL_NOTIFY_MENTION || notifyLevel == CHANNEL_NOTIFY_NONE || notifyLevel == CHANNEL_NOTIFY_QUIET
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
ROLE_SYSTEM_ADMIN = "system_admin"
|
||||
ROLE_SYSTEM_SUPPORT = "system_support"
|
||||
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
|
||||
USER_OFFLINE_TIMEOUT = 5 * 60 * 1000 // 5 minutes
|
||||
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
|
||||
USER_OFFLINE = "offline"
|
||||
USER_AWAY = "away"
|
||||
USER_ONLINE = "online"
|
||||
@@ -147,10 +147,13 @@ func (u *User) SetDefaultNotifications() {
|
||||
u.NotifyProps["email"] = "true"
|
||||
u.NotifyProps["desktop"] = USER_NOTIFY_ALL
|
||||
u.NotifyProps["desktop_sound"] = "true"
|
||||
u.NotifyProps["mention_keys"] = u.Username
|
||||
u.NotifyProps["first_name"] = "true"
|
||||
u.NotifyProps["mention_keys"] = u.Username + ",@" + u.Username
|
||||
u.NotifyProps["first_name"] = "false"
|
||||
u.NotifyProps["all"] = "true"
|
||||
u.NotifyProps["channel"] = "true"
|
||||
splitName := strings.Split(u.FullName, " ")
|
||||
if len(splitName) > 0 && splitName[0] != "" {
|
||||
u.NotifyProps["first_name"] = "true"
|
||||
u.NotifyProps["mention_keys"] += "," + splitName[0]
|
||||
}
|
||||
}
|
||||
@@ -277,17 +280,17 @@ func ComparePassword(hash string, password string) bool {
|
||||
|
||||
func IsUsernameValid(username string) bool {
|
||||
|
||||
var restrictedUsernames = []string {
|
||||
var restrictedUsernames = []string{
|
||||
BOT_USERNAME,
|
||||
"all",
|
||||
"channel",
|
||||
}
|
||||
|
||||
for _,restrictedUsername := range restrictedUsernames {
|
||||
for _, restrictedUsername := range restrictedUsernames {
|
||||
if username == restrictedUsername {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ETAG_ROOT_VERSION = "10"
|
||||
ETAG_ROOT_VERSION = "11"
|
||||
)
|
||||
|
||||
type StringMap map[string]string
|
||||
@@ -260,7 +260,7 @@ func Etag(parts ...interface{}) string {
|
||||
return etag
|
||||
}
|
||||
|
||||
var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_]*[A-Za-z0-9])$`)
|
||||
var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$`)
|
||||
var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`)
|
||||
var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`)
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
|
||||
s.CreateColumnIfNotExists("ChannelMembers", "LastUpdateAt", "NotifyLevel", "bigint(20)", "0") // Remove after 6/7/2015 prod push
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) CreateIndexesIfNotExists() {
|
||||
@@ -273,6 +274,7 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
member.PreSave()
|
||||
if result.Err = member.IsValid(); result.Err != nil {
|
||||
storeChannel <- result
|
||||
return
|
||||
@@ -484,7 +486,8 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelId string, userId string) Sto
|
||||
SET
|
||||
ChannelMembers.MentionCount = 0,
|
||||
ChannelMembers.MsgCount = Channels.TotalMsgCount,
|
||||
ChannelMembers.LastViewedAt = Channels.LastPostAt
|
||||
ChannelMembers.LastViewedAt = Channels.LastPostAt,
|
||||
ChannelMembers.LastUpdateAt = Channels.LastPostAt
|
||||
WHERE
|
||||
Channels.Id = ChannelMembers.ChannelId
|
||||
AND UserId = ?
|
||||
@@ -533,15 +536,18 @@ func (s SqlChannelStore) UpdateNotifyLevel(channelId, userId, notifyLevel string
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
updateAt := model.GetMillis()
|
||||
|
||||
_, err := s.GetMaster().Exec(
|
||||
`UPDATE
|
||||
ChannelMembers
|
||||
SET
|
||||
NotifyLevel = ?
|
||||
NotifyLevel = ?,
|
||||
LastUpdateAt = ?
|
||||
WHERE
|
||||
UserId = ?
|
||||
AND ChannelId = ?`,
|
||||
notifyLevel, userId, channelId)
|
||||
notifyLevel, updateAt, userId, channelId)
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlChannelStore.UpdateNotifyLevel", "We couldn't update the notify level", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
|
||||
}
|
||||
|
||||
@@ -356,9 +356,14 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
termMap := map[string]bool{}
|
||||
|
||||
searchType := "Message"
|
||||
if isHashtagSearch {
|
||||
searchType = "Hashtags"
|
||||
for _,term := range strings.Split(terms, " ") {
|
||||
termMap[term] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// @ has a speical meaning in INNODB FULLTEXT indexes and
|
||||
@@ -394,6 +399,17 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
|
||||
list := &model.PostList{Order: make([]string, 0, len(posts))}
|
||||
|
||||
for _, p := range posts {
|
||||
if searchType == "Hashtags" {
|
||||
exactMatch := false
|
||||
for _, tag := range strings.Split(p.Hashtags, " ") {
|
||||
if termMap[tag] {
|
||||
exactMatch = true
|
||||
}
|
||||
}
|
||||
if !exactMatch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
list.AddPost(p)
|
||||
list.AddOrder(p.Id)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
l4g "code.google.com/p/log4go"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
crand "crypto/rand"
|
||||
dbsql "database/sql"
|
||||
"encoding/base64"
|
||||
@@ -327,20 +330,26 @@ func encrypt(key []byte, text string) (string, error) {
|
||||
}
|
||||
|
||||
plaintext := []byte(text)
|
||||
skey := sha512.Sum512(key)
|
||||
ekey, akey := skey[:32], skey[32:]
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
block, err := aes.NewCipher(ekey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
|
||||
macfn := hmac.New(sha256.New, akey)
|
||||
ciphertext := make([]byte, aes.BlockSize+macfn.Size()+len(plaintext))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(crand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize+macfn.Size():], plaintext)
|
||||
macfn.Write(ciphertext[aes.BlockSize+macfn.Size():])
|
||||
mac := macfn.Sum(nil)
|
||||
copy(ciphertext[aes.BlockSize:aes.BlockSize+macfn.Size()], mac)
|
||||
|
||||
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
@@ -351,9 +360,26 @@ func decrypt(key []byte, cryptoText string) (string, error) {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
|
||||
ciphertext, err := base64.URLEncoding.DecodeString(cryptoText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
skey := sha512.Sum512(key)
|
||||
ekey, akey := skey[:32], skey[32:]
|
||||
macfn := hmac.New(sha256.New, akey)
|
||||
if len(ciphertext) < aes.BlockSize+macfn.Size() {
|
||||
return "", errors.New("short ciphertext")
|
||||
}
|
||||
|
||||
macfn.Write(ciphertext[aes.BlockSize+macfn.Size():])
|
||||
expectedMac := macfn.Sum(nil)
|
||||
mac := ciphertext[aes.BlockSize:aes.BlockSize+macfn.Size()]
|
||||
if hmac.Equal(expectedMac, mac) != true {
|
||||
return "", errors.New("Incorrect MAC for the given ciphertext")
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(ekey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -362,7 +388,7 @@ func decrypt(key []byte, cryptoText string) (string, error) {
|
||||
return "", errors.New("ciphertext too short")
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
ciphertext = ciphertext[aes.BlockSize+macfn.Size():]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"html"
|
||||
)
|
||||
|
||||
func CheckMailSettings() *model.AppError {
|
||||
@@ -84,7 +85,7 @@ func SendMail(to, subject, body string) *model.AppError {
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = fromMail.String()
|
||||
headers["To"] = toMail.String()
|
||||
headers["Subject"] = subject
|
||||
headers["Subject"] = html.UnescapeString(subject)
|
||||
headers["MIME-version"] = "1.0"
|
||||
headers["Content-Type"] = "text/html"
|
||||
|
||||
|
||||
@@ -15,17 +15,8 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
function getExtraInfoStateFromStores() {
|
||||
return {
|
||||
extra_info: ChannelStore.getCurrentExtraInfo()
|
||||
};
|
||||
}
|
||||
|
||||
var ExtraMembers = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addExtraInfoChangeListener(this._onChange);
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
|
||||
var originalLeave = $.fn.popover.Constructor.prototype.leave;
|
||||
$.fn.popover.Constructor.prototype.leave = function(obj) {
|
||||
var self = obj instanceof this.constructor ? obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type);
|
||||
@@ -49,27 +40,21 @@ var ExtraMembers = React.createClass({
|
||||
});
|
||||
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeExtraInfoChangeListener(this._onChange);
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var newState = getExtraInfoStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getExtraInfoStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
var count = this.state.extra_info.members.length == 0 ? "-" : this.state.extra_info.members.length;
|
||||
count = this.state.extra_info.members.length > 19 ? "20+" : count;
|
||||
var count = this.props.members.length == 0 ? "-" : this.props.members.length;
|
||||
count = this.props.members.length > 19 ? "20+" : count;
|
||||
var data_content = "";
|
||||
var sortedMembers = this.props.members;
|
||||
|
||||
this.state.extra_info.members.forEach(function(m) {
|
||||
data_content += "<div style='white-space: nowrap'>" + m.username + "</div>";
|
||||
});
|
||||
if(sortedMembers) {
|
||||
sortedMembers.sort(function(a,b) {
|
||||
return a.username.localeCompare(b.username);
|
||||
})
|
||||
|
||||
sortedMembers.forEach(function(m) {
|
||||
data_content += "<div style='white-space: nowrap'>" + m.username + "</div>";
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{"cursor" : "pointer"}} id="member_popover" data-toggle="popover" data-content={data_content} data-original-title="Members" >
|
||||
@@ -228,7 +213,7 @@ module.exports = React.createClass({
|
||||
<a href="#"><strong className="heading">{channelTitle}</strong></a>
|
||||
}
|
||||
</th>
|
||||
<th><ExtraMembers channelId={this.state.channel.id} /></th>
|
||||
<th><ExtraMembers members={this.state.users} channelId={this.state.channel.id} /></th>
|
||||
{ searchForm }
|
||||
<th>
|
||||
<div className="dropdown" style={{"marginLeft":"5px", "marginRight":"10px"}}>
|
||||
|
||||
@@ -35,9 +35,18 @@ module.exports = React.createClass({
|
||||
<h4 className="modal-title" id="myModalLabel">{channel.display_name}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p><strong>Channel Name: </strong>{channel.display_name}</p>
|
||||
<p><strong>Channel Handle: </strong>{channel.name}</p>
|
||||
<p><strong>Channel ID: </strong>{channel.id}</p>
|
||||
<div className="row form-group">
|
||||
<div className="col-sm-3 info__label">Channel Name: </div>
|
||||
<div className="col-sm-9">{channel.display_name}</div>
|
||||
</div>
|
||||
<div className="row form-group">
|
||||
<div className="col-sm-3 info__label">Channel Handle:</div>
|
||||
<div className="col-sm-9">{channel.name}</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-3 info__label">Channel ID:</div>
|
||||
<div className="col-sm-9">{channel.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var SettingItemMin = require('./setting_item_min.jsx');
|
||||
var SettingItemMax = require('./setting_item_max.jsx');
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
@@ -9,26 +11,50 @@ var ChannelStore = require('../stores/channel_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = e.relatedTarget;
|
||||
var channel_id = button.dataset.channelid;
|
||||
|
||||
var notifyLevel = ChannelStore.getMember(channel_id).notify_level;
|
||||
self.setState({ notify_level: notifyLevel, title: button.dataset.title, channel_id: channel_id });
|
||||
var quietMode = false;
|
||||
if (notifyLevel === "quiet") quietMode = true;
|
||||
self.setState({ notify_level: notifyLevel, quiet_mode: quietMode, title: button.dataset.title, channel_id: channel_id });
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { notify_level: "", title: "", channel_id: "" };
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
handleUpdate: function(e) {
|
||||
_onChange: function() {
|
||||
if (!this.state.channel_id) return;
|
||||
var notifyLevel = ChannelStore.getMember(this.state.channel_id).notify_level;
|
||||
var quietMode = false;
|
||||
if (notifyLevel === "quiet") quietMode = true;
|
||||
|
||||
var newState = this.state;
|
||||
newState.notify_level = notifyLevel;
|
||||
newState.quiet_mode = quietMode;
|
||||
|
||||
if (!utils.areStatesEqual(this.state, newState)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
updateSection: function(section) {
|
||||
this.setState({ activeSection: section });
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { notify_level: "", title: "", channel_id: "", activeSection: "" };
|
||||
},
|
||||
handleUpdate: function() {
|
||||
var channel_id = this.state.channel_id;
|
||||
var notify_level = this.state.notify_level;
|
||||
var notify_level = this.state.quiet_mode ? "quiet" : this.state.notify_level;
|
||||
|
||||
var data = {};
|
||||
data["channel_id"] = channel_id;
|
||||
data["user_id"] = UserStore.getCurrentId();
|
||||
data["notify_level"] = this.state.notify_level;
|
||||
data["notify_level"] = notify_level;
|
||||
|
||||
if (!data["notify_level"] || data["notify_level"].length === 0) return;
|
||||
|
||||
@@ -37,7 +63,7 @@ module.exports = React.createClass({
|
||||
var member = ChannelStore.getMember(channel_id);
|
||||
member.notify_level = notify_level;
|
||||
ChannelStore.setChannelMember(member);
|
||||
$(this.refs.modal.getDOMNode()).modal('hide');
|
||||
this.updateSection("");
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ server_error: err.message });
|
||||
@@ -45,42 +71,138 @@ module.exports = React.createClass({
|
||||
);
|
||||
},
|
||||
handleRadioClick: function(notifyLevel) {
|
||||
this.setState({ notify_level: notifyLevel });
|
||||
this.setState({ notify_level: notifyLevel, quiet_mode: false });
|
||||
this.refs.modal.getDOMNode().focus();
|
||||
},
|
||||
handleQuietToggle: function() {
|
||||
if (this.state.notify_level === "quiet") {
|
||||
this.setState({ notify_level: "none" });
|
||||
this.refs.modal.getDOMNode().focus();
|
||||
} else {
|
||||
this.setState({ notify_level: "quiet" });
|
||||
this.refs.modal.getDOMNode().focus();
|
||||
}
|
||||
handleQuietToggle: function(quietMode) {
|
||||
this.setState({ notify_level: "none", quiet_mode: quietMode });
|
||||
this.refs.modal.getDOMNode().focus();
|
||||
},
|
||||
render: function() {
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
|
||||
var allActive = "";
|
||||
var mentionActive = "";
|
||||
var noneActive = "";
|
||||
var quietActive = "";
|
||||
var desktopHidden = "";
|
||||
var self = this;
|
||||
|
||||
if (this.state.notify_level === "quiet") {
|
||||
desktopHidden = "hidden";
|
||||
quietActive = "active";
|
||||
} else if (this.state.notify_level === "mention") {
|
||||
mentionActive = "active";
|
||||
} else if (this.state.notify_level === "none") {
|
||||
noneActive = "active";
|
||||
var desktopSection;
|
||||
if (this.state.activeSection === 'desktop') {
|
||||
var notifyActive = [false, false, false];
|
||||
if (this.state.notify_level === "mention") {
|
||||
notifyActive[1] = true;
|
||||
} else if (this.state.notify_level === "all") {
|
||||
notifyActive[0] = true;
|
||||
} else {
|
||||
notifyActive[2] = true;
|
||||
}
|
||||
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="radio">
|
||||
<label>
|
||||
<input type="radio" checked={notifyActive[0]} onClick={function(){self.handleRadioClick("all")}}>For all activity</input>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className="radio">
|
||||
<label>
|
||||
<input type="radio" checked={notifyActive[1]} onClick={function(){self.handleRadioClick("mention")}}>Only for mentions</input>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className="radio">
|
||||
<label>
|
||||
<input type="radio" checked={notifyActive[2]} onClick={function(){self.handleRadioClick("none")}}>Never</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
desktopSection = (
|
||||
<SettingItemMax
|
||||
title="Send desktop notifications"
|
||||
inputs={inputs}
|
||||
submit={this.handleUpdate}
|
||||
server_error={server_error}
|
||||
updateSection={function(e){self.updateSection("");self._onChange();e.preventDefault();}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
allActive = "active";
|
||||
var describe = "";
|
||||
if (this.state.notify_level === "mention") {
|
||||
describe = "Only for mentions";
|
||||
} else if (this.state.notify_level === "all") {
|
||||
describe = "For all activity";
|
||||
} else {
|
||||
describe = "Never";
|
||||
}
|
||||
|
||||
desktopSection = (
|
||||
<SettingItemMin
|
||||
title="Send desktop notifications"
|
||||
describe={describe}
|
||||
updateSection={function(e){self.updateSection("desktop");e.preventDefault();}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
var quietSection;
|
||||
if (this.state.activeSection === 'quiet') {
|
||||
var quietActive = ["",""];
|
||||
if (this.state.quiet_mode) {
|
||||
quietActive[0] = "active";
|
||||
} else {
|
||||
quietActive[1] = "active";
|
||||
}
|
||||
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="btn-group" data-toggle="buttons-radio">
|
||||
<button className={"btn btn-default "+quietActive[0]} onClick={function(){self.handleQuietToggle(true)}}>On</button>
|
||||
<button className={"btn btn-default "+quietActive[1]} onClick={function(){self.handleQuietToggle(false)}}>Off</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<br/>
|
||||
Enabling quiet mode will turn off desktop notifications and only mark the channel as unread if you have been mentioned.
|
||||
</div>
|
||||
);
|
||||
|
||||
quietSection = (
|
||||
<SettingItemMax
|
||||
title="Quiet mode"
|
||||
inputs={inputs}
|
||||
submit={this.handleUpdate}
|
||||
server_error={server_error}
|
||||
updateSection={function(e){self.updateSection("");self._onChange();e.preventDefault();}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
var describe = "";
|
||||
if (this.state.quiet_mode) {
|
||||
describe = "On";
|
||||
} else {
|
||||
describe = "Off";
|
||||
}
|
||||
|
||||
quietSection = (
|
||||
<SettingItemMin
|
||||
title="Quiet mode"
|
||||
describe={describe}
|
||||
updateSection={function(e){self.updateSection("quiet");e.preventDefault();}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return (
|
||||
<div className="modal fade" id="channel_notifications" ref="modal" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-dialog settings-modal">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal">
|
||||
@@ -90,31 +212,23 @@ module.exports = React.createClass({
|
||||
<h4 className="modal-title">{"Notification Preferences for " + this.state.title}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className={desktopHidden}>
|
||||
<span>Desktop Notifications</span>
|
||||
<br/>
|
||||
<div className="btn-group" data-toggle="buttons-radio">
|
||||
<button className={"btn btn-default "+allActive} onClick={function(){self.handleRadioClick("all")}}>Any activity (default)</button>
|
||||
<button className={"btn btn-default "+mentionActive} onClick={function(){self.handleRadioClick("mention")}}>Mentions of my name</button>
|
||||
<button className={"btn btn-default "+noneActive} onClick={function(){self.handleRadioClick("none")}}>Nothing</button>
|
||||
<div className="settings-table">
|
||||
<div className="settings-content">
|
||||
<div ref="wrapper" className="user-settings">
|
||||
<br/>
|
||||
<div className="divider-dark first"/>
|
||||
{desktopSection}
|
||||
<div className="divider-light"/>
|
||||
{quietSection}
|
||||
<div className="divider-dark"/>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
<span>Quiet Mode</span>
|
||||
<br/>
|
||||
<div className="btn-group" data-toggle="buttons-checkbox">
|
||||
<button className={"btn btn-default "+quietActive} onClick={this.handleQuietToggle}>Quiet Mode</button>
|
||||
</div>
|
||||
{ server_error }
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-primary" onClick={this.handleUpdate}>Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -112,13 +112,28 @@ module.exports = React.createClass({
|
||||
return { messageText: '', uploadsInProgress: 0, previews: [], submitting: false };
|
||||
},
|
||||
setUploads: function(val) {
|
||||
var num = this.state.uploadsInProgress + val;
|
||||
this.setState({uploadsInProgress: num});
|
||||
var oldInProgress = this.state.uploadsInProgress
|
||||
var newInProgress = oldInProgress + val;
|
||||
|
||||
if (newInProgress + this.state.previews.length > Constants.MAX_UPLOAD_FILES) {
|
||||
newInProgress = Constants.MAX_UPLOAD_FILES - this.state.previews.length;
|
||||
this.setState({limit_error: "Uploads limited to " + Constants.MAX_UPLOAD_FILES + " files maximum. Please use additional comments for more files."});
|
||||
} else {
|
||||
this.setState({limit_error: null});
|
||||
}
|
||||
|
||||
var numToUpload = newInProgress - oldInProgress;
|
||||
if (numToUpload <= 0) return 0;
|
||||
|
||||
this.setState({uploadsInProgress: newInProgress});
|
||||
|
||||
return numToUpload;
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
var post_error = this.state.post_error ? <label className='control-label'>{this.state.post_error}</label> : null;
|
||||
var limit_error = this.state.limit_error ? <div className='has-error'><label className='control-label'>{this.state.limit_error}</label></div> : null;
|
||||
|
||||
var preview = <div/>;
|
||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
|
||||
@@ -129,13 +144,6 @@ module.exports = React.createClass({
|
||||
uploadsInProgress={this.state.uploadsInProgress} />
|
||||
);
|
||||
}
|
||||
var limit_previews = ""
|
||||
if (this.state.previews.length > 5) {
|
||||
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: While all files will be available, only first five will show thumbnails." }</label></div>
|
||||
}
|
||||
if (this.state.previews.length > 20) {
|
||||
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: Uploads limited to 20 files maximum. Please use additional posts for more files." }</label></div>
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
@@ -159,7 +167,7 @@ module.exports = React.createClass({
|
||||
<input type="button" className="btn btn-primary comment-btn pull-right" value="Add Comment" onClick={this.handleSubmit} />
|
||||
{ post_error }
|
||||
{ server_error }
|
||||
{ limit_previews }
|
||||
{ limit_error }
|
||||
</div>
|
||||
</div>
|
||||
{ preview }
|
||||
|
||||
@@ -51,7 +51,7 @@ module.exports = React.createClass({
|
||||
false,
|
||||
function(data) {
|
||||
PostStore.storeDraft(data.channel_id, user_id, null);
|
||||
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null });
|
||||
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null, limit_error: null });
|
||||
|
||||
if (data.goto_location.length > 0) {
|
||||
window.location.href = data.goto_location;
|
||||
@@ -71,7 +71,7 @@ module.exports = React.createClass({
|
||||
client.createPost(post, ChannelStore.getCurrent(),
|
||||
function(data) {
|
||||
PostStore.storeDraft(data.channel_id, data.user_id, null);
|
||||
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null });
|
||||
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null, limit_error: null });
|
||||
this.resizePostHolder();
|
||||
AsyncClient.getPosts(true);
|
||||
|
||||
@@ -207,21 +207,36 @@ module.exports = React.createClass({
|
||||
return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText };
|
||||
},
|
||||
setUploads: function(val) {
|
||||
var num = this.state.uploadsInProgress + val;
|
||||
var oldInProgress = this.state.uploadsInProgress
|
||||
var newInProgress = oldInProgress + val;
|
||||
|
||||
if (newInProgress + this.state.previews.length > Constants.MAX_UPLOAD_FILES) {
|
||||
newInProgress = Constants.MAX_UPLOAD_FILES - this.state.previews.length;
|
||||
this.setState({limit_error: "Uploads limited to " + Constants.MAX_UPLOAD_FILES + " files maximum. Please use additional posts for more files."});
|
||||
} else {
|
||||
this.setState({limit_error: null});
|
||||
}
|
||||
|
||||
var numToUpload = newInProgress - oldInProgress;
|
||||
if (numToUpload <= 0) return 0;
|
||||
|
||||
var draft = PostStore.getCurrentDraft();
|
||||
if (!draft) {
|
||||
draft = {}
|
||||
draft['message'] = '';
|
||||
draft['previews'] = [];
|
||||
}
|
||||
draft['uploadsInProgress'] = num;
|
||||
draft['uploadsInProgress'] = newInProgress;
|
||||
PostStore.storeCurrentDraft(draft);
|
||||
this.setState({uploadsInProgress: num});
|
||||
this.setState({uploadsInProgress: newInProgress});
|
||||
|
||||
return numToUpload;
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
var post_error = this.state.post_error ? <label className='control-label'>{this.state.post_error}</label> : null;
|
||||
var limit_error = this.state.limit_error ? <div className='has-error'><label className='control-label'>{this.state.limit_error}</label></div> : null;
|
||||
|
||||
var preview = <div/>;
|
||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
|
||||
@@ -232,13 +247,6 @@ module.exports = React.createClass({
|
||||
uploadsInProgress={this.state.uploadsInProgress} />
|
||||
);
|
||||
}
|
||||
var limit_previews = ""
|
||||
if (this.state.previews.length > 5) {
|
||||
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: While all files will be available, only first five will show thumbnails." }</label></div>
|
||||
}
|
||||
if (this.state.previews.length > 20) {
|
||||
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: Uploads limited to 20 files maximum. Please use additional posts for more files." }</label></div>
|
||||
}
|
||||
|
||||
return (
|
||||
<form id="create_post" ref="topDiv" role="form" onSubmit={this.handleSubmit}>
|
||||
@@ -260,7 +268,7 @@ module.exports = React.createClass({
|
||||
<div className={post_error ? 'post-create-footer has-error' : 'post-create-footer'}>
|
||||
{ post_error }
|
||||
{ server_error }
|
||||
{ limit_previews }
|
||||
{ limit_error }
|
||||
{ preview }
|
||||
<MsgTyping channelId={this.state.channel_id} parentId=""/>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ var Constants = require('../utils/constants.jsx');
|
||||
module.exports = React.createClass({
|
||||
handleRemove: function(e) {
|
||||
var previewDiv = e.target.parentNode.parentNode;
|
||||
this.props.onRemove(previewDiv.dataset.filename);
|
||||
this.props.onRemove(previewDiv.getAttribute('data-filename'));
|
||||
},
|
||||
render: function() {
|
||||
var previews = [];
|
||||
|
||||
@@ -12,18 +12,18 @@ module.exports = React.createClass({
|
||||
|
||||
this.props.onUploadError(null);
|
||||
|
||||
//This looks redundant, but must be done this way due to
|
||||
//setState being an asynchronous call
|
||||
// This looks redundant, but must be done this way due to
|
||||
// setState being an asynchronous call
|
||||
var numFiles = 0;
|
||||
for(var i = 0; i < files.length && i <= 20 ; i++) {
|
||||
for(var i = 0; i < files.length && i < Constants.MAX_UPLOAD_FILES; i++) {
|
||||
if (files[i].size <= Constants.MAX_FILE_SIZE) {
|
||||
numFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.setUploads(numFiles);
|
||||
var numToUpload = this.props.setUploads(numFiles);
|
||||
|
||||
for (var i = 0; i < files.length && i <= 20; i++) {
|
||||
for (var i = 0; i < files.length && i < numToUpload; i++) {
|
||||
if (files[i].size > Constants.MAX_FILE_SIZE) {
|
||||
this.props.onUploadError("Files must be no more than " + Constants.MAX_FILE_SIZE/1000000 + " MB");
|
||||
continue;
|
||||
@@ -70,8 +70,8 @@ module.exports = React.createClass({
|
||||
|
||||
self.props.onUploadError(null);
|
||||
|
||||
//This looks redundant, but must be done this way due to
|
||||
//setState being an asynchronous call
|
||||
// This looks redundant, but must be done this way due to
|
||||
// setState being an asynchronous call
|
||||
var items = e.clipboardData.items;
|
||||
var numItems = 0;
|
||||
if (items) {
|
||||
@@ -87,9 +87,9 @@ module.exports = React.createClass({
|
||||
}
|
||||
}
|
||||
|
||||
self.props.setUploads(numItems);
|
||||
var numToUpload = self.props.setUploads(numItems);
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
for (var i = 0; i < items.length && i < numToUpload; i++) {
|
||||
if (items[i].type.indexOf("image") !== -1) {
|
||||
var file = items[i].getAsFile();
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ module.exports = React.createClass({
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button data-copy-btn type="button" className="btn btn-primary" data-clipboard-text={this.state.value}>Copy Link</button>
|
||||
<button data-copy-btn type="button" className="btn btn-primary pull-left" data-clipboard-text={this.state.value}>Copy Link</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ var FindTeamDomain = React.createClass({
|
||||
window.location.href = window.location.protocol + "//" + domain + "." + utils.getDomainWithOutSub();
|
||||
}
|
||||
else {
|
||||
this.state.server_error = "We couldn't find your " + strings.TeamPlural + ".";
|
||||
this.state.server_error = "We couldn't find your " + strings.Team + ".";
|
||||
this.setState(this.state);
|
||||
}
|
||||
}.bind(this),
|
||||
|
||||
@@ -6,10 +6,16 @@ module.exports = React.createClass({
|
||||
this.props.handleClick(this.props.username);
|
||||
},
|
||||
render: function() {
|
||||
var icon;
|
||||
if (this.props.id != null) {
|
||||
icon = <span><img className="mention-img" src={"/api/v1/users/" + this.props.id + "/image"}/></span>;
|
||||
} else {
|
||||
icon = <span><i className="mention-img fa fa-users fa-2x"></i></span>;
|
||||
}
|
||||
return (
|
||||
<div className="mentions-name" onClick={this.handleClick}>
|
||||
<img className="pull-left mention-img" src={"/api/v1/users/" + this.props.id + "/image"}/>
|
||||
<span>@{this.props.username}</span><span style={{'color':'grey', 'marginLeft':'10px'}}>{this.props.name}</span>
|
||||
<div className="pull-left">{icon}</div>
|
||||
<div className="pull-left mention-align"><span>@{this.props.username}</span><span className="mention-fullname">{this.props.secondary_text}</span></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ module.exports = React.createClass({
|
||||
}
|
||||
}
|
||||
);
|
||||
$(document).click(function() {
|
||||
if($('#'+self.props.id).length && $('#'+self.props.id).get(0) !== $(':focus').get(0)) {
|
||||
self.setState({mentionText: "-1"})
|
||||
}
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
PostStore.removeMentionDataChangeListener(this._onChange);
|
||||
@@ -74,6 +79,18 @@ module.exports = React.createClass({
|
||||
users.push(profiles[id]);
|
||||
}
|
||||
|
||||
var all = {};
|
||||
all.username = "all";
|
||||
all.full_name = "";
|
||||
all.secondary_text = "Notifies everyone in the team";
|
||||
users.push(all);
|
||||
|
||||
var channel = {};
|
||||
channel.username = "channel";
|
||||
channel.full_name = "";
|
||||
channel.secondary_text = "Notifies everyone in the channel";
|
||||
users.push(channel);
|
||||
|
||||
users.sort(function(a,b) {
|
||||
if (a.username < b.username) return -1;
|
||||
if (a.username > b.username) return 1;
|
||||
@@ -91,6 +108,7 @@ module.exports = React.createClass({
|
||||
var splitName = users[i].full_name.split(' ');
|
||||
firstName = splitName[0].toLowerCase();
|
||||
lastName = splitName.length > 1 ? splitName[splitName.length-1].toLowerCase() : "";
|
||||
users[i].secondary_text = users[i].full_name;
|
||||
}
|
||||
|
||||
if (firstName.lastIndexOf(mentionText,0) === 0
|
||||
@@ -99,7 +117,7 @@ module.exports = React.createClass({
|
||||
<Mention
|
||||
ref={'mention' + index}
|
||||
username={users[i].username}
|
||||
name={users[i].full_name}
|
||||
secondary_text={users[i].secondary_text}
|
||||
id={users[i].id}
|
||||
handleClick={this.handleClick} />
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ module.exports = React.createClass({
|
||||
<span aria-hidden="true">×</span>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 className="modal-title">More Direct Messages</h4>
|
||||
<h4 className="modal-title">More Private Messages</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
|
||||
@@ -6,13 +6,14 @@ var PostBody = require('./post_body.jsx');
|
||||
var PostInfo = require('./post_info.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
$('.edit-modal').on('show.bs.modal', function () {
|
||||
$('.edit-modal .edit-modal-body').css('overflow-y', 'auto');
|
||||
$('.edit-modal .edit-modal-body').css('max-height', $(window).height() * 0.7);
|
||||
$('.modal').on('show.bs.modal', function () {
|
||||
$('.modal-body').css('overflow-y', 'auto');
|
||||
$('.modal-body').css('max-height', $(window).height() * 0.7);
|
||||
});
|
||||
},
|
||||
handleCommentClick: function(e) {
|
||||
@@ -56,7 +57,7 @@ module.exports = React.createClass({
|
||||
|
||||
var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null;
|
||||
|
||||
if(this.props.sameRoot){
|
||||
if (this.props.sameRoot){
|
||||
rootUser = "same--root";
|
||||
}
|
||||
else {
|
||||
@@ -64,13 +65,18 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
var postType = "";
|
||||
if(type != "Post"){
|
||||
if (type != "Post"){
|
||||
postType = "post--comment";
|
||||
}
|
||||
|
||||
var currentUserCss = "";
|
||||
if (UserStore.getCurrentId() === post.user_id) {
|
||||
currentUserCss = "current--user";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id={post.id} className={"post " + this.props.sameUser + " " + rootUser + " " + postType}>
|
||||
<div id={post.id} className={"post " + this.props.sameUser + " " + rootUser + " " + postType + " " + currentUserCss}>
|
||||
{ !this.props.hideProfilePic ?
|
||||
<div className="post-profile-img__container">
|
||||
<img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image"} height="36" width="36" />
|
||||
|
||||
@@ -102,7 +102,7 @@ module.exports = React.createClass({
|
||||
images.push(filenames[i]);
|
||||
} else if (i < Constants.MAX_DISPLAY_FILES) {
|
||||
postFiles.push(
|
||||
<div className="post-image__column custom-file" key={fileInfo.name}>
|
||||
<div className="post-image__column custom-file" key={fileInfo.name+i}>
|
||||
<a href={fileInfo.path+"."+fileInfo.ext} download={fileInfo.name+"."+fileInfo.ext}>
|
||||
<div className={"file-icon "+utils.getIconClassName(type)}/>
|
||||
</a>
|
||||
|
||||
@@ -295,7 +295,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
render: function() {
|
||||
var order = [];
|
||||
var posts = {};
|
||||
var posts;
|
||||
|
||||
var last_viewed = Number.MAX_VALUE;
|
||||
|
||||
@@ -324,13 +324,7 @@ module.exports = React.createClass({
|
||||
if (order.length > 0 && order.length % Constants.POST_CHUNK_SIZE === 0) {
|
||||
more_messages = <a ref="loadmore" className="more-messages-text theme" href="#" onClick={this.getMorePosts}>Load more messages</a>;
|
||||
} else if (channel.type === 'D') {
|
||||
var userIds = channel.name.split('__');
|
||||
var teammate;
|
||||
if (userIds.length === 2 && userIds[0] === user_id) {
|
||||
teammate = UserStore.getProfile(userIds[1]);
|
||||
} else if (userIds.length === 2 && userIds[1] === user_id) {
|
||||
teammate = UserStore.getProfile(userIds[0]);
|
||||
}
|
||||
var teammate = utils.getDirectTeammate(channel.id)
|
||||
|
||||
if (teammate) {
|
||||
var teammate_name = teammate.full_name.length > 0 ? teammate.full_name : teammate.username;
|
||||
@@ -342,13 +336,13 @@ module.exports = React.createClass({
|
||||
<div className="channel-intro-profile">
|
||||
<strong><UserProfile userId={teammate.id} /></strong>
|
||||
</div>
|
||||
<p className="channel-intro-text">{"This is the start of your direct message history with " + teammate_name + "." }<br/>{"Direct messages and files shared here are not shown to people outside this area."}</p>
|
||||
<p className="channel-intro-text">{"This is the start of your private message history with " + teammate_name + "." }<br/>{"Private messages and files shared here are not shown to people outside this area."}</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
more_messages = (
|
||||
<div className="channel-intro">
|
||||
<p className="channel-intro-text">{"This is the start of your direct message history with this " + strings.Team + "mate. Direct messages and files shared here are not shown to people outside this area."}</p>
|
||||
<p className="channel-intro-text">{"This is the start of your private message history with this " + strings.Team + "mate. Private messages and files shared here are not shown to people outside this area."}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -356,6 +350,7 @@ module.exports = React.createClass({
|
||||
var ui_name = channel.display_name
|
||||
var members = ChannelStore.getCurrentExtraInfo().members;
|
||||
var creator_name = "";
|
||||
var userStyle = { color: UserStore.getCurrentUser().props.theme }
|
||||
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
if (members[i].roles.indexOf('admin') > -1) {
|
||||
@@ -382,8 +377,18 @@ module.exports = React.createClass({
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
|
||||
more_messages = (
|
||||
<div className="channel-intro">
|
||||
<h4 className="channel-intro-title">Welcome</h4>
|
||||
<p>
|
||||
{"This is the start of " + ui_name + ", a channel for conversations you’d prefer out of more focused channels."}
|
||||
<br/>
|
||||
<a className="intro-links" href="#" style={userStyle} data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={ui_name} data-channelid={channel.id}><i className="fa fa-pencil"></i>Set a description</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
var userStyle = { color: UserStore.getCurrentUser().props.theme }
|
||||
var ui_type = channel.type === 'P' ? "private group" : "channel";
|
||||
more_messages = (
|
||||
<div className="channel-intro">
|
||||
@@ -403,55 +408,70 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
var postCtls = [];
|
||||
var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date();
|
||||
var currentPostDay = new Date();
|
||||
|
||||
for (var i = order.length-1; i >= 0; i--) {
|
||||
var post = posts[order[i]];
|
||||
var parentPost;
|
||||
if (posts != undefined) {
|
||||
var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date();
|
||||
var currentPostDay = new Date();
|
||||
|
||||
if (post.parent_id) {
|
||||
parentPost = posts[post.parent_id];
|
||||
} else {
|
||||
parentPost = null;
|
||||
for (var i = order.length-1; i >= 0; i--) {
|
||||
var post = posts[order[i]];
|
||||
var parentPost;
|
||||
|
||||
if (post.parent_id) {
|
||||
parentPost = posts[post.parent_id];
|
||||
} else {
|
||||
parentPost = null;
|
||||
}
|
||||
|
||||
var sameUser = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && post.create_at - posts[order[i+1]].create_at <= 1000*60*5 ? "same--user" : "";
|
||||
var sameRoot = i < order.length-1 && post.root_id != "" && (posts[order[i+1]].id === post.root_id || posts[order[i+1]].root_id === post.root_id) ? true : false;
|
||||
|
||||
// we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
|
||||
var hideProfilePic = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && posts[order[i+1]].root_id === '' && post.root_id === '';
|
||||
|
||||
// check if it's the last comment in a consecutive string of comments on the same post
|
||||
var isLastComment = false;
|
||||
if (utils.isComment(post)) {
|
||||
// it is the last comment if it is last post in the channel or the next post has a different root post
|
||||
isLastComment = (i === 0 || posts[order[i-1]].root_id != post.root_id);
|
||||
}
|
||||
|
||||
var postCtl = <Post sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} />;
|
||||
|
||||
currentPostDay = utils.getDateForUnixTicks(post.create_at);
|
||||
if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) {
|
||||
postCtls.push(
|
||||
<div className="date-separator">
|
||||
<hr className="separator__hr" />
|
||||
<div className="separator__text">{currentPostDay.toDateString()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (post.create_at > last_viewed && !rendered_last_viewed) {
|
||||
rendered_last_viewed = true;
|
||||
postCtls.push(
|
||||
<div className="new-separator">
|
||||
<hr id="new_message" className="separator__hr" />
|
||||
<div className="separator__text">New Messages</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
postCtls.push(postCtl);
|
||||
previousPostDay = utils.getDateForUnixTicks(post.create_at);
|
||||
}
|
||||
|
||||
var sameUser = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && post.create_at - posts[order[i+1]].create_at <= 1000*60*5 ? "same--user" : "";
|
||||
var sameRoot = i < order.length-1 && post.root_id != "" && (posts[order[i+1]].id === post.root_id || posts[order[i+1]].root_id === post.root_id) ? true : false;
|
||||
|
||||
// we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
|
||||
var hideProfilePic = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && posts[order[i+1]].root_id === '' && post.root_id === '';
|
||||
|
||||
// check if it's the last comment in a consecutive string of comments on the same post
|
||||
var isLastComment = false;
|
||||
if (utils.isComment(post)) {
|
||||
// it is the last comment if it is last post in the channel or the next post has a different root post
|
||||
isLastComment = (i === 0 || posts[order[i-1]].root_id != post.root_id);
|
||||
}
|
||||
|
||||
var postCtl = <Post sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} />;
|
||||
|
||||
currentPostDay = utils.getDateForUnixTicks(post.create_at);
|
||||
if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) {
|
||||
postCtls.push(
|
||||
<div className="date-separator">
|
||||
<hr className="separator__hr" />
|
||||
<div className="separator__text">{currentPostDay.toDateString()}</div>
|
||||
}
|
||||
else {
|
||||
postCtls.push(
|
||||
<div ref="loadingscreen" className="loading-screen">
|
||||
<div className="loading__content">
|
||||
<h3>Loading</h3>
|
||||
<div id="round_1" className="round"></div>
|
||||
<div id="round_2" className="round"></div>
|
||||
<div id="round_3" className="round"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (post.create_at > last_viewed && !rendered_last_viewed) {
|
||||
rendered_last_viewed = true;
|
||||
postCtls.push(
|
||||
<div className="new-separator">
|
||||
<hr id="new_message" className="separator__hr" />
|
||||
<div className="separator__text">New Messages</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
postCtls.push(postCtl);
|
||||
previousPostDay = utils.getDateForUnixTicks(post.create_at);
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -68,9 +68,14 @@ RootPost = React.createClass({
|
||||
var filenames = this.props.post.filenames;
|
||||
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
|
||||
|
||||
var type = "Post"
|
||||
var type = "Post";
|
||||
if (this.props.post.root_id.length > 0) {
|
||||
type = "Comment"
|
||||
type = "Comment";
|
||||
}
|
||||
|
||||
var currentUserCss = "";
|
||||
if (UserStore.getCurrentId() === this.props.post.user_id) {
|
||||
currentUserCss = "current--user";
|
||||
}
|
||||
|
||||
if (filenames) {
|
||||
@@ -84,7 +89,7 @@ RootPost = React.createClass({
|
||||
if (fileSplit.length < 2) continue;
|
||||
|
||||
var ext = fileSplit[fileSplit.length-1];
|
||||
fileSplit.splice(fileSplit.length-1,1)
|
||||
fileSplit.splice(fileSplit.length-1,1);
|
||||
var filePath = fileSplit.join('.');
|
||||
var filename = filePath.split('/')[filePath.split('/').length-1];
|
||||
|
||||
@@ -111,7 +116,7 @@ RootPost = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="post post--root">
|
||||
<div className={"post post--root " + currentUserCss}>
|
||||
<div className="post-profile-img__container">
|
||||
<img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" />
|
||||
</div>
|
||||
@@ -170,6 +175,11 @@ CommentPost = React.createClass({
|
||||
|
||||
var commentClass = "post";
|
||||
|
||||
var currentUserCss = "";
|
||||
if (UserStore.getCurrentId() === this.props.post.user_id) {
|
||||
currentUserCss = "current--user";
|
||||
}
|
||||
|
||||
var postImageModalId = "rhs_comment_view_image_modal_" + this.props.post.id;
|
||||
var filenames = this.props.post.filenames;
|
||||
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
|
||||
@@ -219,7 +229,7 @@ CommentPost = React.createClass({
|
||||
var message = utils.textToJsx(this.props.post.message);
|
||||
|
||||
return (
|
||||
<div className={commentClass}>
|
||||
<div className={commentClass + " " + currentUserCss}>
|
||||
<div className="post-profile-img__container">
|
||||
<img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" />
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,7 @@ SearchItem = React.createClass({
|
||||
e.preventDefault();
|
||||
|
||||
var self = this;
|
||||
|
||||
client.getPost(
|
||||
this.props.post.channel_id,
|
||||
this.props.post.id,
|
||||
@@ -64,6 +65,11 @@ SearchItem = React.createClass({
|
||||
dispatchError(err, "getPost");
|
||||
}
|
||||
);
|
||||
|
||||
var postChannel = ChannelStore.get(this.props.post.channel_id);
|
||||
var teammate = postChannel.type === 'D' ? utils.getDirectTeammate(this.props.post.channel_id).username : "";
|
||||
|
||||
utils.switchChannel(postChannel,teammate);
|
||||
},
|
||||
render: function() {
|
||||
|
||||
@@ -73,7 +79,7 @@ SearchItem = React.createClass({
|
||||
|
||||
if (channel) {
|
||||
if (channel.type === 'D') {
|
||||
channelName = "Direct Message";
|
||||
channelName = "Private Message";
|
||||
} else {
|
||||
channelName = channel.display_name;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ module.exports = React.createClass({
|
||||
<li className="col-sm-12 section-title">{this.props.title}</li>
|
||||
<li className="col-sm-9 col-sm-offset-3">
|
||||
<ul className="setting-list">
|
||||
<li className="row setting-list-item form-group">
|
||||
<li className="setting-list-item">
|
||||
{inputs}
|
||||
</li>
|
||||
<li className="setting-list-item">
|
||||
|
||||
@@ -269,13 +269,8 @@ var SidebarLoggedIn = React.createClass({
|
||||
var channel = ChannelStore.getCurrent();
|
||||
if (channel) {
|
||||
if (channel.type === 'D') {
|
||||
userIds = channel.name.split('__');
|
||||
if (userIds.length < 2) return;
|
||||
if (userIds[0] == UserStore.getCurrentId() && UserStore.getProfile(userIds[1])) {
|
||||
document.title = UserStore.getProfile(userIds[1]).username + " " + document.title.substring(document.title.lastIndexOf("-"));
|
||||
} else if (userIds[1] == UserStore.getCurrentId() && UserStore.getProfile(userIds[0])) {
|
||||
document.title = UserStore.getProfile(userIds[0]).username + " " + document.title.substring(document.title.lastIndexOf("-"));
|
||||
}
|
||||
var teammate_username = utils.getDirectTeammate(channel.id).username
|
||||
document.title = teammate_username + " " + document.title.substring(document.title.lastIndexOf("-"));
|
||||
} else {
|
||||
document.title = channel.display_name + " " + document.title.substring(document.title.lastIndexOf("-"))
|
||||
}
|
||||
@@ -414,7 +409,7 @@ var SidebarLoggedIn = React.createClass({
|
||||
{privateChannelItems}
|
||||
</ul>
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
<li><h4>Direct Messages</h4></li>
|
||||
<li><h4>Private Messages</h4></li>
|
||||
{directMessageItems}
|
||||
{ this.state.hideDirectChannels.length > 0 ?
|
||||
<li><a href="#" data-toggle="modal" className="nav-more" data-target="#more_direct_channels" data-channels={JSON.stringify(this.state.hideDirectChannels)}>{"More ("+this.state.hideDirectChannels.length+")"}</a></li>
|
||||
|
||||
@@ -60,6 +60,7 @@ module.exports = React.createClass({
|
||||
<div className="nav-pills__container">
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
<li><a href="#" data-toggle="modal" data-target="#user_settings1"><i className="glyphicon glyphicon-cog"></i>Account Settings</a></li>
|
||||
{ isAdmin ? <li><a href="#" data-toggle="modal" data-target="#team_settings"><i className="glyphicon glyphicon-globe"></i>Team Settings</a></li> : "" }
|
||||
{ invite_link }
|
||||
{ team_link }
|
||||
{ manage_link }
|
||||
|
||||
@@ -164,7 +164,9 @@ TeamUrlPage = React.createClass({
|
||||
}
|
||||
|
||||
var cleaned_name = utils.cleanUpUrlable(name);
|
||||
if (cleaned_name != name) {
|
||||
|
||||
var urlRegex = /^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g;
|
||||
if (cleaned_name != name || !urlRegex.test(name)) {
|
||||
this.setState({name_error: "Must be lowercase alphanumeric characters"});
|
||||
return;
|
||||
}
|
||||
@@ -337,7 +339,7 @@ EmailItem = React.createClass({
|
||||
return false;
|
||||
}
|
||||
else if (email === teamEmail) {
|
||||
this.state.email_error = "Please use an a different email than the one used at signup";
|
||||
this.state.email_error = "Please use a different email than the one used at signup";
|
||||
this.setState(this.state);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -78,13 +78,14 @@ var FeatureTab = React.createClass({
|
||||
<button className={"btn btn-default "+valetActive[0]} onClick={function(){self.handleValetRadio("true")}}>On</button>
|
||||
<button className={"btn btn-default "+valetActive[1]} onClick={function(){self.handleValetRadio("false")}}>Off</button>
|
||||
</div>
|
||||
<div><br/>Warning: Turning on the Valet feature and using it with any third party software increases the risk of a security breach.</div>
|
||||
<div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div>
|
||||
<br></br>
|
||||
</div>
|
||||
);
|
||||
|
||||
valetSection = (
|
||||
<SettingItemMax
|
||||
title="Valet"
|
||||
title="Valet (Preview - EXPERTS ONLY)"
|
||||
inputs={inputs}
|
||||
submit={this.submitValetFeature}
|
||||
server_error={server_error}
|
||||
@@ -102,7 +103,7 @@ var FeatureTab = React.createClass({
|
||||
|
||||
valetSection = (
|
||||
<SettingItemMin
|
||||
title="Valet"
|
||||
title="Valet (Preview - EXPERTS ONLY)"
|
||||
describe={describe}
|
||||
updateSection={function(){self.props.updateSection("valet");}}
|
||||
/>
|
||||
@@ -113,7 +114,7 @@ var FeatureTab = React.createClass({
|
||||
<div>
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title" ref="title"><i className="modal-back"></i>General Settings</h4>
|
||||
<h4 className="modal-title" ref="title"><i className="modal-back"></i>Feature Settings</h4>
|
||||
</div>
|
||||
<div ref="wrapper" className="user-settings">
|
||||
<h3 className="tab-header">Feature Settings</h3>
|
||||
|
||||
@@ -45,7 +45,7 @@ module.exports = React.createClass({
|
||||
updateTab={this.updateTab}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-content">
|
||||
<div className="settings-content minimize-settings">
|
||||
<TeamSettings
|
||||
activeTab={this.state.active_tab}
|
||||
activeSection={this.state.active_section}
|
||||
|
||||
@@ -153,7 +153,7 @@ module.exports = React.createClass({
|
||||
var mentions = [];
|
||||
for (var i = 0; i < matches.length; i++) {
|
||||
var m = matches[i].substring(1,matches[i].length).trim();
|
||||
if (m in profileMap && mentions.indexOf(m) === -1) {
|
||||
if ((m in profileMap && mentions.indexOf(m) === -1) || Constants.SPECIAL_MENTIONS.indexOf(m) !== -1) {
|
||||
mentions.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ function getStateFromStores(userId) {
|
||||
|
||||
if (profile == null) {
|
||||
return { profile: { id: "0", username: "..."} };
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return { profile: profile };
|
||||
}
|
||||
}
|
||||
@@ -54,12 +53,11 @@ module.exports = React.createClass({
|
||||
var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username;
|
||||
|
||||
|
||||
var data_content = ""
|
||||
data_content += "<img style='margin: 10px' src='/api/v1/users/" + this.state.profile.id + "/image' height='128' width='128' />"
|
||||
var data_content = "<img style='margin: 10px' src='/api/v1/users/" + this.state.profile.id + "/image' height='128' width='128' />";
|
||||
if (!config.ShowEmail) {
|
||||
data_content += "<div><span style='white-space:nowrap;'>Email not shared</span></div>";
|
||||
data_content += "<div class='text-nowrap'>Email not shared</div>";
|
||||
} else {
|
||||
data_content += "<div><a href='mailto:'" + this.state.profile.email + "'' style='white-space:nowrap;text-transform:lowercase;'>" + this.state.profile.email + "</a></div>";
|
||||
data_content += "<div><a href='mailto:" + this.state.profile.email + "' class='text-nowrap text-lowercase'>" + this.state.profile.email + "</a></div>";
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,11 +20,10 @@ function getNotificationsStateFromStores() {
|
||||
var mention_key = false;
|
||||
var custom_keys = "";
|
||||
var first_name_key = false;
|
||||
var all_key = false;
|
||||
var channel_key = false;
|
||||
|
||||
if (!user.notify_props) {
|
||||
mention_keys = user.username;
|
||||
if (user.full_name.length > 0) mention_keys += ","+ user.full_name.split(" ")[0];
|
||||
} else {
|
||||
if (user.notify_props) {
|
||||
if (user.notify_props.mention_keys !== undefined) {
|
||||
var keys = user.notify_props.mention_keys.split(',');
|
||||
|
||||
@@ -48,9 +47,17 @@ function getNotificationsStateFromStores() {
|
||||
if (user.notify_props.first_name !== undefined) {
|
||||
first_name_key = user.notify_props.first_name === "true";
|
||||
}
|
||||
|
||||
if (user.notify_props.all !== undefined) {
|
||||
all_key = user.notify_props.all === "true";
|
||||
}
|
||||
|
||||
if (user.notify_props.channel !== undefined) {
|
||||
channel_key = user.notify_props.channel === "true";
|
||||
}
|
||||
}
|
||||
|
||||
return { notify_level: desktop, enable_email: email, enable_sound: sound, username_key: username_key, mention_key: mention_key, custom_keys: custom_keys, custom_keys_checked: custom_keys.length > 0, first_name_key: first_name_key };
|
||||
return { notify_level: desktop, enable_email: email, enable_sound: sound, username_key: username_key, mention_key: mention_key, custom_keys: custom_keys, custom_keys_checked: custom_keys.length > 0, first_name_key: first_name_key, all_key: all_key, channel_key: channel_key };
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +80,8 @@ var NotificationsTab = React.createClass({
|
||||
|
||||
data["mention_keys"] = string_keys;
|
||||
data["first_name"] = this.state.first_name_key ? "true" : "false";
|
||||
data["all"] = this.state.all_key ? "true" : "false";
|
||||
data["channel"] = this.state.channel_key ? "true" : "false";
|
||||
|
||||
client.updateUserNotifyProps(data,
|
||||
function(data) {
|
||||
@@ -120,6 +129,12 @@ var NotificationsTab = React.createClass({
|
||||
updateFirstNameKey: function(val) {
|
||||
this.setState({ first_name_key: val });
|
||||
},
|
||||
updateAllKey: function(val) {
|
||||
this.setState({ all_key: val });
|
||||
},
|
||||
updateChannelKey: function(val) {
|
||||
this.setState({ channel_key: val });
|
||||
},
|
||||
updateCustomMentionKeys: function() {
|
||||
var checked = this.refs.customcheck.getDOMNode().checked;
|
||||
|
||||
@@ -155,7 +170,7 @@ var NotificationsTab = React.createClass({
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div className="col-sm-12">
|
||||
<div>
|
||||
<div className="radio">
|
||||
<label>
|
||||
<input type="radio" checked={notifyActive[0]} onClick={function(){self.handleNotifyRadio("all")}}>For all activity</input>
|
||||
@@ -164,7 +179,7 @@ var NotificationsTab = React.createClass({
|
||||
</div>
|
||||
<div className="radio">
|
||||
<label>
|
||||
<input type="radio" checked={notifyActive[1]} onClick={function(){self.handleNotifyRadio("mention")}}>Only for mentions and direct messages</input>
|
||||
<input type="radio" checked={notifyActive[1]} onClick={function(){self.handleNotifyRadio("mention")}}>Only for mentions and private messages</input>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
@@ -188,7 +203,7 @@ var NotificationsTab = React.createClass({
|
||||
} else {
|
||||
var describe = "";
|
||||
if (this.state.notify_level === "mention") {
|
||||
describe = "Only for mentions and direct messages";
|
||||
describe = "Only for mentions and private messages";
|
||||
} else if (this.state.notify_level === "none") {
|
||||
describe = "Never";
|
||||
} else {
|
||||
@@ -216,7 +231,7 @@ var NotificationsTab = React.createClass({
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div className="col-sm-12">
|
||||
<div>
|
||||
<div className="btn-group" data-toggle="buttons-radio">
|
||||
<button className={"btn btn-default "+soundActive[0]} onClick={function(){self.handleSoundRadio("true")}}>On</button>
|
||||
<button className={"btn btn-default "+soundActive[1]} onClick={function(){self.handleSoundRadio("false")}}>Off</button>
|
||||
@@ -262,12 +277,12 @@ var NotificationsTab = React.createClass({
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div className="col-sm-12">
|
||||
<div>
|
||||
<div className="btn-group" data-toggle="buttons-radio">
|
||||
<button className={"btn btn-default "+emailActive[0]} onClick={function(){self.handleEmailRadio("true")}}>On</button>
|
||||
<button className={"btn btn-default "+emailActive[1]} onClick={function(){self.handleEmailRadio("false")}}>Off</button>
|
||||
</div>
|
||||
<div><br/>{"Email notifications are sent for mentions and direct messages after you have been away from " + config.SiteName + " for 5 minutes."}</div>
|
||||
<div><br/>{"Email notifications are sent for mentions and private messages after you have been away from " + config.SiteName + " for 5 minutes."}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -309,7 +324,7 @@ var NotificationsTab = React.createClass({
|
||||
|
||||
if (first_name != "") {
|
||||
inputs.push(
|
||||
<div className="col-sm-12">
|
||||
<div>
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.first_name_key} onChange={function(e){self.updateFirstNameKey(e.target.checked);}}>{'Your case sensitive first name "' + first_name + '"'}</input>
|
||||
@@ -320,7 +335,7 @@ var NotificationsTab = React.createClass({
|
||||
}
|
||||
|
||||
inputs.push(
|
||||
<div className="col-sm-12">
|
||||
<div>
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.username_key} onChange={function(e){self.updateUsernameKey(e.target.checked);}}>{'Your non-case sensitive username "' + user.username + '"'}</input>
|
||||
@@ -330,7 +345,7 @@ var NotificationsTab = React.createClass({
|
||||
);
|
||||
|
||||
inputs.push(
|
||||
<div className="col-sm-12">
|
||||
<div>
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.mention_key} onChange={function(e){self.updateMentionKey(e.target.checked);}}>{'Your username mentioned "@' + user.username + '"'}</input>
|
||||
@@ -340,7 +355,27 @@ var NotificationsTab = React.createClass({
|
||||
);
|
||||
|
||||
inputs.push(
|
||||
<div className="col-sm-12">
|
||||
<div>
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.all_key} onChange={function(e){self.updateAllKey(e.target.checked);}}>{'Team-wide mentions "@all"'}</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.channel_key} onChange={function(e){self.updateChannelKey(e.target.checked);}}>{'Channel-wide mentions "@channel"'}</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input ref="customcheck" type="checkbox" checked={this.state.custom_keys_checked} onChange={this.updateCustomMentionKeys}>{'Other non-case sensitive words, separated by commas:'}</input>
|
||||
@@ -369,6 +404,8 @@ var NotificationsTab = React.createClass({
|
||||
}
|
||||
if (this.state.username_key) keys.push(this.props.user.username);
|
||||
if (this.state.mention_key) keys.push('@'+this.props.user.username);
|
||||
if (this.state.all_key) keys.push('@all');
|
||||
if (this.state.channel_key) keys.push('@channel');
|
||||
if (this.state.custom_keys.length > 0) keys = keys.concat(this.state.custom_keys.split(','));
|
||||
|
||||
var describe = "";
|
||||
@@ -622,7 +659,7 @@ var SecurityTab = React.createClass({
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-5 control-label">Current Password</label>
|
||||
<div className="col-sm-7">
|
||||
<input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/>
|
||||
@@ -630,7 +667,7 @@ var SecurityTab = React.createClass({
|
||||
</div>
|
||||
);
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-5 control-label">New Password</label>
|
||||
<div className="col-sm-7">
|
||||
<input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/>
|
||||
@@ -638,7 +675,7 @@ var SecurityTab = React.createClass({
|
||||
</div>
|
||||
);
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-5 control-label">Retype New Password</label>
|
||||
<div className="col-sm-7">
|
||||
<input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/>
|
||||
@@ -772,6 +809,11 @@ var GeneralTab = React.createClass({
|
||||
|
||||
if(!this.submitActive) return;
|
||||
|
||||
if(this.state.picture.type !== "image/jpeg") {
|
||||
this.setState({client_error: "Only JPG images may be used for profile pictures"});
|
||||
return;
|
||||
}
|
||||
|
||||
formData = new FormData();
|
||||
formData.append('image', this.state.picture, this.state.picture.name);
|
||||
|
||||
@@ -802,11 +844,13 @@ var GeneralTab = React.createClass({
|
||||
updatePicture: function(e) {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
this.setState({ picture: e.target.files[0] });
|
||||
|
||||
this.submitActive = true;
|
||||
this.setState({client_error:null})
|
||||
|
||||
} else {
|
||||
this.setState({ picture: null });
|
||||
}
|
||||
|
||||
this.submitActive = true
|
||||
},
|
||||
updateSection: function(section) {
|
||||
this.setState({client_error:""})
|
||||
@@ -837,7 +881,7 @@ var GeneralTab = React.createClass({
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-5 control-label">First Name</label>
|
||||
<div className="col-sm-7">
|
||||
<input className="form-control" type="text" onChange={this.updateFirstName} value={this.state.first_name}/>
|
||||
@@ -846,7 +890,7 @@ var GeneralTab = React.createClass({
|
||||
);
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-5 control-label">Last Name</label>
|
||||
<div className="col-sm-7">
|
||||
<input className="form-control" type="text" onChange={this.updateLastName} value={this.state.last_name}/>
|
||||
@@ -879,7 +923,7 @@ var GeneralTab = React.createClass({
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-5 control-label">{utils.isMobile() ? "": "Username"}</label>
|
||||
<div className="col-sm-7">
|
||||
<input className="form-control" type="text" onChange={this.updateUsername} value={this.state.username}/>
|
||||
@@ -911,7 +955,7 @@ var GeneralTab = React.createClass({
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-5 control-label">Primary Email</label>
|
||||
<div className="col-sm-7">
|
||||
<input className="form-control" type="text" onChange={this.updateEmail} value={this.state.email}/>
|
||||
@@ -947,7 +991,7 @@ var GeneralTab = React.createClass({
|
||||
submit={this.submitPicture}
|
||||
src={"/api/v1/users/" + user.id + "/image"}
|
||||
server_error={server_error}
|
||||
client_error={email_error}
|
||||
client_error={client_error}
|
||||
updateSection={function(e){self.updateSection("");e.preventDefault();}}
|
||||
picture={this.state.picture}
|
||||
pictureChange={this.updatePicture}
|
||||
@@ -1048,7 +1092,7 @@ var AppearanceTab = React.createClass({
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<li className="row setting-list-item form-group">
|
||||
<li className="setting-list-item">
|
||||
<div className="btn-group" data-toggle="buttons-radio">
|
||||
{ theme_buttons }
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@ module.exports = React.createClass({
|
||||
updateTab={this.updateTab}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-content">
|
||||
<div className="settings-content minimize-settings">
|
||||
<UserSettings
|
||||
activeTab={this.state.active_tab}
|
||||
activeSection={this.state.active_section}
|
||||
|
||||
@@ -240,6 +240,9 @@ var UserStore = assign({}, EventEmitter.prototype, {
|
||||
if (first.length > 0) keys.push(first);
|
||||
}
|
||||
|
||||
if (user.notify_props.all === "true") keys.push('@all');
|
||||
if (user.notify_props.channel === "true") keys.push('@channel');
|
||||
|
||||
return keys;
|
||||
} else {
|
||||
return [];
|
||||
|
||||
@@ -36,6 +36,7 @@ module.exports = {
|
||||
SERVER_ACTION: null,
|
||||
VIEW_ACTION: null
|
||||
}),
|
||||
SPECIAL_MENTIONS: ['all', 'channel'],
|
||||
CHARACTER_LIMIT: 4000,
|
||||
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png'],
|
||||
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac'],
|
||||
@@ -48,8 +49,10 @@ module.exports = {
|
||||
PATCH_TYPES: ['patch'],
|
||||
ICON_FROM_TYPE: {'audio': 'audio', 'video': 'video', 'spreadsheet': 'ppt', 'pdf': 'pdf', 'code': 'code' , 'word': 'word' , 'excel': 'excel' , 'patch': 'patch', 'other': 'generic'},
|
||||
MAX_DISPLAY_FILES: 5,
|
||||
MAX_UPLOAD_FILES: 5,
|
||||
MAX_FILE_SIZE: 50000000, // 50 MB
|
||||
DEFAULT_CHANNEL: 'town-square',
|
||||
OFFTOPIC_CHANNEL: 'off-topic',
|
||||
POST_CHUNK_SIZE: 60,
|
||||
RESERVED_DOMAINS: [
|
||||
"www",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx')
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
@@ -395,14 +396,10 @@ module.exports.textToJsx = function(text, options) {
|
||||
|
||||
var inner = [];
|
||||
|
||||
// Function specific regexes
|
||||
var hashRegex = /^href="#[^"]+"|(#[A-Za-z]+[A-Za-z0-9_]*[A-Za-z0-9])$/g;
|
||||
// Function specific regex
|
||||
var hashRegex = /^href="#[^"]+"|(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$/g;
|
||||
|
||||
var implicitKeywords = {};
|
||||
var keywordArray = UserStore.getCurrentMentionKeys();
|
||||
for (var i = 0; i < keywordArray.length; i++) {
|
||||
implicitKeywords[keywordArray[i]] = true;
|
||||
}
|
||||
var implicitKeywords = UserStore.getCurrentMentionKeys();
|
||||
|
||||
var lines = text.split("\n");
|
||||
var urlMatcher = new LinkifyIt();
|
||||
@@ -421,10 +418,13 @@ module.exports.textToJsx = function(text, options) {
|
||||
highlightSearchClass = " search-highlight";
|
||||
}
|
||||
|
||||
if (explicitMention && UserStore.getProfileByUsername(explicitMention[1])) {
|
||||
if (explicitMention &&
|
||||
(UserStore.getProfileByUsername(explicitMention[1]) ||
|
||||
Constants.SPECIAL_MENTIONS.indexOf(explicitMention[1]) !== -1))
|
||||
{
|
||||
var name = explicitMention[1];
|
||||
// do both a non-case sensitive and case senstive check
|
||||
var mClass = (name.toLowerCase() in implicitKeywords || name in implicitKeywords) ? mentionClass : "";
|
||||
var mClass = implicitKeywords.indexOf('@'+name.toLowerCase()) !== -1 || implicitKeywords.indexOf('@'+name) !== -1 ? mentionClass : "";
|
||||
|
||||
var suffix = word.match(puncEndRegex);
|
||||
var prefix = word.match(puncStartRegex);
|
||||
@@ -446,7 +446,7 @@ module.exports.textToJsx = function(text, options) {
|
||||
} else if (trimWord.match(hashRegex)) {
|
||||
var suffix = word.match(puncEndRegex);
|
||||
var prefix = word.match(puncStartRegex);
|
||||
var mClass = trimWord in implicitKeywords || trimWord.toLowerCase() in implicitKeywords ? mentionClass : "";
|
||||
var mClass = implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1 ? mentionClass : "";
|
||||
|
||||
if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) {
|
||||
highlightSearchClass = " search-highlight";
|
||||
@@ -454,7 +454,7 @@ module.exports.textToJsx = function(text, options) {
|
||||
|
||||
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_hash"} className={"theme " + mClass + highlightSearchClass} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(trimWord)}>{trimWord}</a>{suffix} </span>);
|
||||
|
||||
} else if (trimWord in implicitKeywords || trimWord.toLowerCase() in implicitKeywords) {
|
||||
} else if (implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1) {
|
||||
var suffix = word.match(puncEndRegex);
|
||||
var prefix = word.match(puncStartRegex);
|
||||
|
||||
@@ -727,6 +727,25 @@ module.exports.isComment = function(post) {
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports.getDirectTeammate = function(channel_id) {
|
||||
var userIds = ChannelStore.get(channel_id).name.split('__');
|
||||
var curUserId = UserStore.getCurrentId();
|
||||
var teammate = {};
|
||||
|
||||
if(userIds.length != 2 || userIds.indexOf(curUserId) === -1) {
|
||||
return teammate;
|
||||
}
|
||||
|
||||
for (var idx in userIds) {
|
||||
if(userIds[idx] !== curUserId) {
|
||||
teammate = UserStore.getProfile(userIds[idx]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return teammate;
|
||||
}
|
||||
|
||||
Image.prototype.load = function(url, progressCallback) {
|
||||
var thisImg = this;
|
||||
var xmlHTTP = new XMLHttpRequest();
|
||||
|
||||
@@ -3,7 +3,7 @@ html, body {
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #e9e9e9;
|
||||
position: relative;
|
||||
@@ -11,21 +11,29 @@ body {
|
||||
&.white {
|
||||
background: #fff;
|
||||
.inner__wrap {
|
||||
height: 100%;
|
||||
> .row.content {
|
||||
min-height: 100%;
|
||||
margin-bottom: -89px;
|
||||
}
|
||||
}
|
||||
.row.content {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
.inner__wrap {
|
||||
height: 100%;
|
||||
> .row.main {
|
||||
height: 100%;
|
||||
margin-bottom: -89px;
|
||||
}
|
||||
}
|
||||
> .container-fluid {
|
||||
@include clearfix;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a {
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -142,7 +150,7 @@ div.theme {
|
||||
top: 0;
|
||||
color: #FFF;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
float:left;
|
||||
}
|
||||
.channel-intro-title {
|
||||
font-weight:bold;
|
||||
font-weight:600;
|
||||
}
|
||||
.channel-intro-text {
|
||||
margin-top:35px;
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
.dropdown-menu {
|
||||
li a {
|
||||
padding: 3 20px;
|
||||
padding: 3px 20px;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
68
web/sass-files/sass/partials/_loading.scss
Обычный файл
68
web/sass-files/sass/partials/_loading.scss
Обычный файл
@@ -0,0 +1,68 @@
|
||||
.loading-screen {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
@include box-sizing(border-box);
|
||||
text-align: center;
|
||||
.loading__content {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
h3 {
|
||||
font-weight: 400;
|
||||
margin: 0 0.2em 0;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-screen {
|
||||
.loading__content {
|
||||
.round {
|
||||
background-color: #444;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
display: inline-block;
|
||||
margin: 0 1px;
|
||||
opacity: 0.1;
|
||||
@include border-radius(10px);
|
||||
-moz-animation: move 0.75s infinite linear;
|
||||
-webkit-animation: move 0.75s infinite linear;
|
||||
}
|
||||
|
||||
#round_1 {
|
||||
-moz-animation-delay: .2s;
|
||||
-webkit-animation-delay: .2s;
|
||||
}
|
||||
|
||||
#round_2 {
|
||||
-moz-animation-delay: .4s;
|
||||
-webkit-animation-delay: .4s;
|
||||
}
|
||||
|
||||
#round_3 {
|
||||
-moz-animation-delay: .6s;
|
||||
-webkit-animation-delay: .6s;
|
||||
}
|
||||
|
||||
@-moz-keyframes move {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
};
|
||||
}
|
||||
|
||||
@-webkit-keyframes move {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,19 @@
|
||||
background: $primary-color;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding-bottom: 1px;
|
||||
padding-bottom: 2px;
|
||||
@include border-radius(3px);
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mentions--top {
|
||||
position: absolute;
|
||||
z-index:99999;
|
||||
z-index: 1060;
|
||||
.mentions-box {
|
||||
position:absolute;
|
||||
background-color:#fff;
|
||||
border:1px solid #ddd;
|
||||
overflow:scroll;
|
||||
border: $border-gray;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
bottom:0;
|
||||
}
|
||||
}
|
||||
@@ -29,10 +27,10 @@
|
||||
height:37px;
|
||||
padding:2px;
|
||||
z-index:101;
|
||||
}
|
||||
|
||||
.mentions-name:hover {
|
||||
background-color:#e8eaed;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color:#e8eaed;
|
||||
}
|
||||
}
|
||||
|
||||
.mentions-text {
|
||||
@@ -46,6 +44,11 @@
|
||||
border-radius: 10%;
|
||||
}
|
||||
|
||||
.mention-fullname {
|
||||
color: grey;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.mention-highlight {
|
||||
background-color:#fff2bb;
|
||||
}
|
||||
@@ -53,3 +56,8 @@
|
||||
.mention-link {
|
||||
color:$primary-color;
|
||||
}
|
||||
|
||||
.mention-align {
|
||||
position:relative;
|
||||
top:5px;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
.modal-body {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
.modal {
|
||||
&.image_modal {
|
||||
.modal-backdrop.in {
|
||||
@include opacity(0.7);
|
||||
}
|
||||
}
|
||||
.info__label {
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
padding-right: 0;
|
||||
}
|
||||
.remove__member {
|
||||
float: right;
|
||||
}
|
||||
@@ -13,7 +21,6 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
.modal-body {
|
||||
max-height: 75%;
|
||||
overflow: auto;
|
||||
}
|
||||
.modal-push-down {
|
||||
@@ -29,7 +36,7 @@
|
||||
border-radius: 0;
|
||||
background: $primary-color;
|
||||
color: #FFF;
|
||||
padding: 15px 15px 11px;
|
||||
padding: 15px 15px 11px;
|
||||
border: none;
|
||||
min-height: 56px;
|
||||
@include clearfix;
|
||||
@@ -41,14 +48,23 @@
|
||||
margin: 0;
|
||||
}
|
||||
button.close {
|
||||
margin-top: 0;
|
||||
margin: -2px -2px 0 0;
|
||||
color: #fff;
|
||||
@include opacity(1);
|
||||
z-index: 5;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
@include single-transition(all, 0.25s, ease-in);
|
||||
position: relative;
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
span {
|
||||
line-height: 10px;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
margin-right: 10px;
|
||||
&.btn-primary {
|
||||
float: right;
|
||||
margin-top: -4px;
|
||||
@@ -102,7 +118,7 @@
|
||||
}
|
||||
.more-channel-name {
|
||||
color: #444;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
tbody {
|
||||
@@ -127,17 +143,19 @@
|
||||
.modal-image {
|
||||
position:relative;
|
||||
width:100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
.image-wrapper {
|
||||
padding: 4px;
|
||||
background: #FFF;
|
||||
position: relative;
|
||||
max-width: 80%;
|
||||
min-height: 280px;
|
||||
min-width: 280px;
|
||||
@include border-radius(4px);
|
||||
@include border-radius(3px);
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
&:hover {
|
||||
@include border-radius(3px 3px 0 0);
|
||||
}
|
||||
&:hover .modal-close {
|
||||
@include opacity(1);
|
||||
}
|
||||
@@ -217,10 +235,11 @@
|
||||
}
|
||||
.modal-button-bar {
|
||||
position:absolute;
|
||||
bottom:0px;
|
||||
bottom:-40px;
|
||||
left:0px;
|
||||
right:0px;
|
||||
background-color:rgba(0, 0, 0, 0.8);
|
||||
background-color: #222;
|
||||
@include border-radius(0 0 3px 3px);
|
||||
@include opacity(0);
|
||||
-webkit-transition: opacity 0.6s;
|
||||
-moz-transition: opacity 0.6s;
|
||||
@@ -228,7 +247,6 @@
|
||||
transition: opacity 0.6s;
|
||||
line-height: 40px;
|
||||
padding: 0 10px;
|
||||
margin: 4px;
|
||||
&.footer--show {
|
||||
@include opacity(1);
|
||||
}
|
||||
|
||||
@@ -86,10 +86,11 @@ body.ios {
|
||||
background: #FFF;
|
||||
display: inline-block;
|
||||
padding: 0 1em;
|
||||
font-weight: 900;
|
||||
font-weight: 700;
|
||||
@include border-radius(50px);
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
.new-separator {
|
||||
@@ -212,6 +213,12 @@ body.ios {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.current--user {
|
||||
.post-body {
|
||||
@include border-radius(0 4px 4px 0);
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
&.same--root {
|
||||
.comment-icon__container {
|
||||
@include opacity(0);
|
||||
@@ -249,6 +256,7 @@ body.ios {
|
||||
}
|
||||
p {
|
||||
margin: 0 0 5px;
|
||||
font-size: 0.97em;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.comment-icon__container {
|
||||
@@ -269,7 +277,7 @@ body.ios {
|
||||
float: left;
|
||||
.post-profile-img {
|
||||
margin-right: 10px;
|
||||
@include border-radius(3px);
|
||||
@include border-radius(50px);
|
||||
}
|
||||
}
|
||||
&.post__content {
|
||||
@@ -290,6 +298,8 @@ body.ios {
|
||||
width: 600px;
|
||||
float: left;
|
||||
word-wrap: break-word;
|
||||
padding: 0.3em 0.5em 0.1em;
|
||||
margin: -0.3em 0 0;
|
||||
.post-link {
|
||||
@include clearfix;
|
||||
text-overflow: ellipsis;
|
||||
@@ -359,7 +369,7 @@ body.ios {
|
||||
.embed-title {
|
||||
margin: 3px 0 1px;
|
||||
color: #555;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
&.post--comment {
|
||||
&.other--root {
|
||||
.post-comment {
|
||||
margin-left: 0;
|
||||
margin-left: -7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,16 @@
|
||||
width: 825px;
|
||||
}
|
||||
.post-body {
|
||||
padding-left: 0;
|
||||
width: 736px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.post-body {
|
||||
float: none;
|
||||
width: 750px;
|
||||
margin: 0;
|
||||
}
|
||||
.post__content {
|
||||
width: 920px;
|
||||
}
|
||||
@@ -63,10 +68,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.post-body {
|
||||
float: none;
|
||||
width: 750px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +82,7 @@
|
||||
&.post--comment {
|
||||
&.other--root {
|
||||
.post-comment {
|
||||
margin-left: 0;
|
||||
margin-left: -7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +90,7 @@
|
||||
margin-left: 60px;
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid #EEE;
|
||||
margin-bottom: 10px;
|
||||
div.post-profile-img__container {
|
||||
.post-profile-img {
|
||||
display: none;
|
||||
@@ -98,7 +100,7 @@
|
||||
width: 825px;
|
||||
}
|
||||
.post-body {
|
||||
padding-left: 0;
|
||||
width: 736px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -135,6 +137,7 @@
|
||||
}
|
||||
}
|
||||
.post-body {
|
||||
margin: 0;
|
||||
float: none;
|
||||
width: 750px;
|
||||
}
|
||||
@@ -231,17 +234,14 @@
|
||||
}
|
||||
}
|
||||
.modal {
|
||||
.info__label {
|
||||
text-align: left;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.modal-header {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
.modal-action {
|
||||
margin-top: 10px;
|
||||
}
|
||||
button.close {
|
||||
width: 35px;
|
||||
height: 32px;
|
||||
margin: -5px -10px 0;
|
||||
}
|
||||
.modal-title {
|
||||
float: none;
|
||||
}
|
||||
@@ -261,9 +261,11 @@
|
||||
.settings-table {
|
||||
display: block;
|
||||
.settings-content {
|
||||
display: block;
|
||||
.section-edit {
|
||||
text-align: left;
|
||||
&.minimize-settings {
|
||||
display: block;
|
||||
.section-edit {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
.settings-links {
|
||||
@@ -287,10 +289,12 @@
|
||||
}
|
||||
.settings-table {
|
||||
.settings-content {
|
||||
padding: 0;
|
||||
display: none;
|
||||
.user-settings {
|
||||
padding: 70px 20px 30px;
|
||||
&.minimize-settings {
|
||||
padding: 0;
|
||||
display: none;
|
||||
.user-settings {
|
||||
padding: 70px 20px 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.settings-links {
|
||||
@@ -538,7 +542,6 @@
|
||||
.modal {
|
||||
.modal-image {
|
||||
.image-wrapper {
|
||||
padding-bottom: 40px;
|
||||
.modal-close {
|
||||
@include opacity(1);
|
||||
}
|
||||
@@ -575,7 +578,7 @@
|
||||
&.post--comment {
|
||||
&.other--root {
|
||||
.post-comment {
|
||||
margin-left: 11px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
border: none;
|
||||
}
|
||||
.search-channel__name {
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-edit {
|
||||
@@ -172,7 +172,7 @@
|
||||
border-top:1px solid lightgrey;
|
||||
}
|
||||
.post-profile-img {
|
||||
@include border-radius(3px);
|
||||
@include border-radius(50px);
|
||||
margin-right: 8px;
|
||||
}
|
||||
.member-name {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
color: #999;
|
||||
&.unread-title {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
&:hover, &:focus {
|
||||
background: #e6f2fa;
|
||||
@@ -70,6 +70,7 @@
|
||||
color: #111;
|
||||
background-color: #e1e1e1;
|
||||
border-radius: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
@import "partials/modal";
|
||||
@import "partials/mentions";
|
||||
@import "partials/error";
|
||||
@import "partials/loading";
|
||||
|
||||
// Responsive Css
|
||||
@import "partials/responsive";
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
{{define "channel"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "find_team"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body class="white">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">
|
||||
<link href='https://fonts.googleapis.com/css?family=Lato:400,700,900' rel='stylesheet' type='text/css'>
|
||||
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,600,700' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" href="/static/css/styles.css">
|
||||
|
||||
<script src="/static/js/min/perfect-scrollbar.min.js"></script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "home"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "login"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body class="white">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "password_reset"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body class="white">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "signup_team"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body class="white">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "signup_team_complete"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body class="white">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "signup_team_confirm"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body class="white">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "signup_user_complete"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body class="white">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "verify"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "welcome"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body>
|
||||
|
||||
@@ -285,7 +285,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
||||
otherUserId = ids[0]
|
||||
}
|
||||
|
||||
if sc, err := api.CreateDirectChannel(c, otherUserId, r.URL.Path); err != nil {
|
||||
if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil {
|
||||
api.Handle404(w, r)
|
||||
return
|
||||
} else {
|
||||
|
||||
Ссылка в новой задаче
Block a user