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.
|
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
|
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.
|
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)
|
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
|
6. When docker is done fetching the image, open http://dockerhost:8065/ in your browser
|
||||||
|
|
||||||
### Ubuntu ###
|
### 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`
|
2. Run `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium`
|
||||||
|
|
||||||
`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
|
|
||||||
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
|
||||||
|
|
||||||
### Arch ###
|
### 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`
|
3. When docker is done fetching the image, open http://localhost:8065/ in your browser.
|
||||||
|
|
||||||
`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
|
|
||||||
|
|
||||||
### Notes ###
|
### Notes ###
|
||||||
If your ISP blocks port 25 then you may install locally but email will not be sent.
|
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`
|
1. `docker stop mattermost-dev`
|
||||||
2. `docker rm -v 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)
|
AWS Elastic Beanstalk Setup (Docker)
|
||||||
------------------------------------
|
------------------------------------
|
||||||
@@ -89,7 +99,7 @@ AWS Elastic Beanstalk Setup (Docker)
|
|||||||
2. Select "Create New Application" from the top right.
|
2. Select "Create New Application" from the top right.
|
||||||
3. Name the application and press next
|
3. Name the application and press next
|
||||||
4. Select "Create a web server" environment.
|
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.
|
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.
|
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.
|
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.
|
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
|
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.
|
20. Return the Elastic Beanstalk from the AWS console.
|
||||||
21. Select the environment you created.
|
21. Select the environment you created.
|
||||||
22. Select configuration from the sidebar.
|
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.
|
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.
|
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
|
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.
|
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
|
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
|
c.Err = err
|
||||||
return
|
return
|
||||||
} else {
|
} 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 {
|
if result := <-Srv.Store.Channel().Save(channel); result.Err != nil {
|
||||||
return nil, result.Err
|
return nil, result.Err
|
||||||
} else {
|
} else {
|
||||||
@@ -100,7 +100,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if sc, err := CreateDirectChannel(c, userId, r.URL.Path); err != nil {
|
if sc, err := CreateDirectChannel(c, userId); err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
} else {
|
} 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 {
|
if len(otherUserId) != 26 {
|
||||||
return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId)
|
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)
|
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
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId,
|
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) {
|
func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
channel := model.ChannelFromJson(r.Body)
|
channel := model.ChannelFromJson(r.Body)
|
||||||
@@ -303,7 +320,7 @@ func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
channelId := params["id"]
|
channelId := params["id"]
|
||||||
|
|
||||||
JoinChannel(c, channelId, r.URL.Path)
|
JoinChannel(c, channelId, "")
|
||||||
|
|
||||||
if c.Err != nil {
|
if c.Err != nil {
|
||||||
return
|
return
|
||||||
@@ -314,7 +331,7 @@ func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(model.MapToJson(result)))
|
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)
|
sc := Srv.Store.Channel().Get(channelId)
|
||||||
uc := Srv.Store.User().Get(c.Session.UserId)
|
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 {
|
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 {
|
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
|
||||||
c.Err = cmresult.Err
|
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) {
|
func leaveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
|
|||||||
@@ -35,8 +35,15 @@ func TestCreateChannel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rget := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
|
rget := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
|
||||||
if rget.Channels[0].Name != channel.Name {
|
nameMatch := false
|
||||||
t.Fatal("full name didn't match")
|
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 {
|
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["user_id"] = user.Id
|
||||||
data["notify_level"] = model.CHANNEL_NOTIFY_MENTION
|
data["notify_level"] = model.CHANNEL_NOTIFY_MENTION
|
||||||
|
|
||||||
|
timeBeforeUpdate := model.GetMillis()
|
||||||
|
|
||||||
if _, err := Client.UpdateNotifyLevel(data); err != nil {
|
if _, err := Client.UpdateNotifyLevel(data); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -689,6 +698,10 @@ func TestUpdateNotifyLevel(t *testing.T) {
|
|||||||
t.Fatal("NotifyLevel did not update properly")
|
t.Fatal("NotifyLevel did not update properly")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
|
||||||
|
t.Fatal("LastUpdateAt did not update")
|
||||||
|
}
|
||||||
|
|
||||||
data["user_id"] = "junk"
|
data["user_id"] = "junk"
|
||||||
if _, err := Client.UpdateNotifyLevel(data); err == nil {
|
if _, err := Client.UpdateNotifyLevel(data); err == nil {
|
||||||
t.Fatal("Should have errored - bad user id")
|
t.Fatal("Should have errored - bad user id")
|
||||||
@@ -735,7 +748,7 @@ func TestUpdateNotifyLevel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFuzzyChannel(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 := &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)
|
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")
|
Client.LoginByEmail(team.Domain, user.Email, "pwd")
|
||||||
|
|
||||||
// Strings that should pass as acceptable channel names
|
// Strings that should pass as acceptable channel names
|
||||||
var fuzzyStringsPass = []string {
|
var fuzzyStringsPass = []string{
|
||||||
"*", "?", ".", "}{][)(><", "{}[]()<>",
|
"*", "?", ".", "}{][)(><", "{}[]()<>",
|
||||||
|
|
||||||
"qahwah ( قهوة)",
|
"qahwah ( قهوة)",
|
||||||
"שָׁלוֹם עֲלֵיכֶם",
|
"שָׁלוֹם עֲלֵיכֶם",
|
||||||
"Ramen チャーシュー chāshū",
|
"Ramen チャーシュー chāshū",
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ func joinCommand(c *Context, command *model.Command) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
JoinChannel(c, v.Id, "/command")
|
JoinChannel(c, v.Id, "")
|
||||||
|
|
||||||
if c.Err != nil {
|
if c.Err != nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func TestJoinCommands(t *testing.T) {
|
|||||||
|
|
||||||
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
|
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")
|
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)
|
channel = result.Data.(*model.Channel)
|
||||||
if channel.Type == model.CHANNEL_DIRECT {
|
if channel.Type == model.CHANNEL_DIRECT {
|
||||||
bodyText = "You have one new message."
|
bodyText = "You have one new message."
|
||||||
subjectText = "New Direct Message"
|
subjectText = "New Private Message"
|
||||||
} else {
|
} else {
|
||||||
bodyText = "You have one new mention."
|
bodyText = "You have one new mention."
|
||||||
subjectText = "New Mention"
|
subjectText = "New Mention"
|
||||||
@@ -273,7 +273,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
|
|||||||
|
|
||||||
} else {
|
} 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 {
|
if eResult := <-echan; eResult.Err != nil {
|
||||||
l4g.Error("Failed to get channel members channel_id=%v err=%v", post.ChannelId, eResult.Err.Message)
|
l4g.Error("Failed to get channel members channel_id=%v err=%v", post.ChannelId, eResult.Err.Message)
|
||||||
return
|
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
|
// Build a map as a list of unique user_ids that are mentioned in this post
|
||||||
splitF := func(c rune) bool {
|
splitF := func(c rune) bool {
|
||||||
return model.SplitRunes[c]
|
return model.SplitRunes[c]
|
||||||
}
|
}
|
||||||
splitMessage := strings.FieldsFunc(strings.Replace(post.Message, "<br>", " ", -1), splitF)
|
splitMessage := strings.FieldsFunc(post.Message, splitF)
|
||||||
for _, word := range splitMessage {
|
for _, word := range splitMessage {
|
||||||
|
|
||||||
// Non-case-sensitive check for regular keys
|
// 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 {
|
} else {
|
||||||
rteam := result.Data.(*model.Team)
|
rteam := result.Data.(*model.Team)
|
||||||
|
|
||||||
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id}
|
if _, err := CreateDefaultChannels(c, rteam.Id); err != nil {
|
||||||
|
c.Err = nil
|
||||||
if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil {
|
|
||||||
c.Err = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +195,8 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
rteam := result.Data.(*model.Team)
|
rteam := result.Data.(*model.Team)
|
||||||
|
|
||||||
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id}
|
if _, err := CreateDefaultChannels(c, rteam.Id); err != nil {
|
||||||
|
c.Err = nil
|
||||||
if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil {
|
|
||||||
c.Err = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,12 +484,21 @@ func InviteMembers(team *model.Team, user *model.User, invites []string) {
|
|||||||
} else {
|
} else {
|
||||||
sender = user.FullName
|
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 := NewServerTemplatePage("invite_subject", teamUrl)
|
||||||
subjectPage.Props["SenderName"] = sender
|
subjectPage.Props["SenderName"] = sender
|
||||||
subjectPage.Props["TeamName"] = team.Name
|
subjectPage.Props["TeamName"] = team.Name
|
||||||
bodyPage := NewServerTemplatePage("invite_body", teamUrl)
|
bodyPage := NewServerTemplatePage("invite_body", teamUrl)
|
||||||
bodyPage.Props["TeamName"] = team.Name
|
bodyPage.Props["TeamName"] = team.Name
|
||||||
bodyPage.Props["SenderName"] = sender
|
bodyPage.Props["SenderName"] = sender
|
||||||
|
bodyPage.Props["SenderStatus"] = senderRole
|
||||||
|
|
||||||
bodyPage.Props["Email"] = invite
|
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"
|
ts.Data = "garbage"
|
||||||
_, err = Client.CreateTeamFromSignup(&ts)
|
_, err = Client.CreateTeamFromSignup(&ts)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -71,6 +76,17 @@ func TestCreateTeam(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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 {
|
if rteam.Data.(*model.Team).Name != team.Name {
|
||||||
t.Fatal("full name didn't match")
|
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">
|
<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://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>
|
<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">
|
<link rel="stylesheet" href="/static/css/styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="white error">
|
<body class="white error">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
|
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
|
||||||
<h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2>
|
<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">
|
<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>
|
<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>
|
</p>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h2 style="font-weight: normal; margin-top: 10px;">You were mentioned</h2>
|
<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>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">
|
<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>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 {
|
} else {
|
||||||
ruser := result.Data.(*model.User)
|
ruser := result.Data.(*model.User)
|
||||||
|
|
||||||
// Do not error if user cannot be added to the town-square channel
|
// Soft error if there is an issue joining the default channels
|
||||||
if cresult := <-Srv.Store.Channel().GetByName(team.Id, "town-square"); cresult.Err != nil {
|
if err := JoinDefaultChannels(c, ruser, channelRole); err != nil {
|
||||||
l4g.Error("Failed to get town-square err=%v", cresult.Err)
|
l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//fireAndForgetWelcomeEmail(strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl+"/channels/town-square")
|
//fireAndForgetWelcomeEmail(strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl+"/channels/town-square")
|
||||||
|
|
||||||
if user.EmailVerified {
|
if user.EmailVerified {
|
||||||
if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
|
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 {
|
} else {
|
||||||
FireAndForgetVerifyEmail(result.Data.(*model.User).Id, strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl)
|
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{})
|
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)
|
message := model.NewMessage(team.Id, "", ruser.Id, model.ACTION_NEW_USER)
|
||||||
|
|
||||||
store.PublishAndForget(message)
|
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
|
// *IMPORTANT* - Kind of hacky
|
||||||
// This should be the last function in any test file
|
// This should be the last function in any test file
|
||||||
// that calls Setup()
|
// that calls Setup()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"ServiceSettings": {
|
"ServiceSettings": {
|
||||||
"SiteName": "Mattermost",
|
"SiteName": "Mattermost",
|
||||||
"Domain": "",
|
"Domain": "",
|
||||||
"Mode" : "prod",
|
"Mode" : "dev",
|
||||||
"AllowTesting" : false,
|
"AllowTesting" : false,
|
||||||
"UseSSL": false,
|
"UseSSL": false,
|
||||||
"Port": "80",
|
"Port": "80",
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
createdTeam := result.Data.(*model.Team)
|
createdTeam := result.Data.(*model.Team)
|
||||||
|
|
||||||
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: createdTeam.Id}
|
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
|
c.Err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ func (o *ChannelList) Etag() string {
|
|||||||
t = member.LastViewedAt
|
t = member.LastViewedAt
|
||||||
id = v.Id
|
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"`
|
MsgCount int64 `json:"msg_count"`
|
||||||
MentionCount int64 `json:"mention_count"`
|
MentionCount int64 `json:"mention_count"`
|
||||||
NotifyLevel string `json:"notify_level"`
|
NotifyLevel string `json:"notify_level"`
|
||||||
|
LastUpdateAt int64 `json:"last_update_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ChannelMember) ToJson() string {
|
func (o *ChannelMember) ToJson() string {
|
||||||
@@ -70,6 +71,10 @@ func (o *ChannelMember) IsValid() *AppError {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *ChannelMember) PreSave() {
|
||||||
|
o.LastUpdateAt = GetMillis()
|
||||||
|
}
|
||||||
|
|
||||||
func IsChannelNotifyLevelValid(notifyLevel string) bool {
|
func IsChannelNotifyLevelValid(notifyLevel string) bool {
|
||||||
return notifyLevel == CHANNEL_NOTIFY_ALL || notifyLevel == CHANNEL_NOTIFY_MENTION || notifyLevel == CHANNEL_NOTIFY_NONE || notifyLevel == CHANNEL_NOTIFY_QUIET
|
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_ADMIN = "system_admin"
|
||||||
ROLE_SYSTEM_SUPPORT = "system_support"
|
ROLE_SYSTEM_SUPPORT = "system_support"
|
||||||
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
|
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_OFFLINE = "offline"
|
||||||
USER_AWAY = "away"
|
USER_AWAY = "away"
|
||||||
USER_ONLINE = "online"
|
USER_ONLINE = "online"
|
||||||
@@ -147,10 +147,13 @@ func (u *User) SetDefaultNotifications() {
|
|||||||
u.NotifyProps["email"] = "true"
|
u.NotifyProps["email"] = "true"
|
||||||
u.NotifyProps["desktop"] = USER_NOTIFY_ALL
|
u.NotifyProps["desktop"] = USER_NOTIFY_ALL
|
||||||
u.NotifyProps["desktop_sound"] = "true"
|
u.NotifyProps["desktop_sound"] = "true"
|
||||||
u.NotifyProps["mention_keys"] = u.Username
|
u.NotifyProps["mention_keys"] = u.Username + ",@" + u.Username
|
||||||
u.NotifyProps["first_name"] = "true"
|
u.NotifyProps["first_name"] = "false"
|
||||||
|
u.NotifyProps["all"] = "true"
|
||||||
|
u.NotifyProps["channel"] = "true"
|
||||||
splitName := strings.Split(u.FullName, " ")
|
splitName := strings.Split(u.FullName, " ")
|
||||||
if len(splitName) > 0 && splitName[0] != "" {
|
if len(splitName) > 0 && splitName[0] != "" {
|
||||||
|
u.NotifyProps["first_name"] = "true"
|
||||||
u.NotifyProps["mention_keys"] += "," + splitName[0]
|
u.NotifyProps["mention_keys"] += "," + splitName[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,17 +280,17 @@ func ComparePassword(hash string, password string) bool {
|
|||||||
|
|
||||||
func IsUsernameValid(username string) bool {
|
func IsUsernameValid(username string) bool {
|
||||||
|
|
||||||
var restrictedUsernames = []string {
|
var restrictedUsernames = []string{
|
||||||
BOT_USERNAME,
|
BOT_USERNAME,
|
||||||
"all",
|
"all",
|
||||||
"channel",
|
"channel",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _,restrictedUsername := range restrictedUsernames {
|
for _, restrictedUsername := range restrictedUsernames {
|
||||||
if username == restrictedUsername {
|
if username == restrictedUsername {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ETAG_ROOT_VERSION = "10"
|
ETAG_ROOT_VERSION = "11"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StringMap map[string]string
|
type StringMap map[string]string
|
||||||
@@ -260,7 +260,7 @@ func Etag(parts ...interface{}) string {
|
|||||||
return etag
|
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 puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`)
|
||||||
var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`)
|
var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
|
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
|
||||||
|
s.CreateColumnIfNotExists("ChannelMembers", "LastUpdateAt", "NotifyLevel", "bigint(20)", "0") // Remove after 6/7/2015 prod push
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlChannelStore) CreateIndexesIfNotExists() {
|
func (s SqlChannelStore) CreateIndexesIfNotExists() {
|
||||||
@@ -273,6 +274,7 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
|
|||||||
go func() {
|
go func() {
|
||||||
result := StoreResult{}
|
result := StoreResult{}
|
||||||
|
|
||||||
|
member.PreSave()
|
||||||
if result.Err = member.IsValid(); result.Err != nil {
|
if result.Err = member.IsValid(); result.Err != nil {
|
||||||
storeChannel <- result
|
storeChannel <- result
|
||||||
return
|
return
|
||||||
@@ -484,7 +486,8 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelId string, userId string) Sto
|
|||||||
SET
|
SET
|
||||||
ChannelMembers.MentionCount = 0,
|
ChannelMembers.MentionCount = 0,
|
||||||
ChannelMembers.MsgCount = Channels.TotalMsgCount,
|
ChannelMembers.MsgCount = Channels.TotalMsgCount,
|
||||||
ChannelMembers.LastViewedAt = Channels.LastPostAt
|
ChannelMembers.LastViewedAt = Channels.LastPostAt,
|
||||||
|
ChannelMembers.LastUpdateAt = Channels.LastPostAt
|
||||||
WHERE
|
WHERE
|
||||||
Channels.Id = ChannelMembers.ChannelId
|
Channels.Id = ChannelMembers.ChannelId
|
||||||
AND UserId = ?
|
AND UserId = ?
|
||||||
@@ -533,15 +536,18 @@ func (s SqlChannelStore) UpdateNotifyLevel(channelId, userId, notifyLevel string
|
|||||||
go func() {
|
go func() {
|
||||||
result := StoreResult{}
|
result := StoreResult{}
|
||||||
|
|
||||||
|
updateAt := model.GetMillis()
|
||||||
|
|
||||||
_, err := s.GetMaster().Exec(
|
_, err := s.GetMaster().Exec(
|
||||||
`UPDATE
|
`UPDATE
|
||||||
ChannelMembers
|
ChannelMembers
|
||||||
SET
|
SET
|
||||||
NotifyLevel = ?
|
NotifyLevel = ?,
|
||||||
|
LastUpdateAt = ?
|
||||||
WHERE
|
WHERE
|
||||||
UserId = ?
|
UserId = ?
|
||||||
AND ChannelId = ?`,
|
AND ChannelId = ?`,
|
||||||
notifyLevel, userId, channelId)
|
notifyLevel, updateAt, userId, channelId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Err = model.NewAppError("SqlChannelStore.UpdateNotifyLevel", "We couldn't update the notify level", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
|
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() {
|
go func() {
|
||||||
result := StoreResult{}
|
result := StoreResult{}
|
||||||
|
termMap := map[string]bool{}
|
||||||
|
|
||||||
searchType := "Message"
|
searchType := "Message"
|
||||||
if isHashtagSearch {
|
if isHashtagSearch {
|
||||||
searchType = "Hashtags"
|
searchType = "Hashtags"
|
||||||
|
for _,term := range strings.Split(terms, " ") {
|
||||||
|
termMap[term] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ has a speical meaning in INNODB FULLTEXT indexes and
|
// @ 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))}
|
list := &model.PostList{Order: make([]string, 0, len(posts))}
|
||||||
|
|
||||||
for _, p := range 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.AddPost(p)
|
||||||
list.AddOrder(p.Id)
|
list.AddOrder(p.Id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import (
|
|||||||
l4g "code.google.com/p/log4go"
|
l4g "code.google.com/p/log4go"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
dbsql "database/sql"
|
dbsql "database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@@ -327,20 +330,26 @@ func encrypt(key []byte, text string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plaintext := []byte(text)
|
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 {
|
if err != nil {
|
||||||
return "", err
|
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]
|
iv := ciphertext[:aes.BlockSize]
|
||||||
if _, err := io.ReadFull(crand.Reader, iv); err != nil {
|
if _, err := io.ReadFull(crand.Reader, iv); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
stream := cipher.NewCFBEncrypter(block, iv)
|
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
|
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||||
}
|
}
|
||||||
@@ -351,9 +360,26 @@ func decrypt(key []byte, cryptoText string) (string, error) {
|
|||||||
return "{}", nil
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -362,7 +388,7 @@ func decrypt(key []byte, cryptoText string) (string, error) {
|
|||||||
return "", errors.New("ciphertext too short")
|
return "", errors.New("ciphertext too short")
|
||||||
}
|
}
|
||||||
iv := ciphertext[:aes.BlockSize]
|
iv := ciphertext[:aes.BlockSize]
|
||||||
ciphertext = ciphertext[aes.BlockSize:]
|
ciphertext = ciphertext[aes.BlockSize+macfn.Size():]
|
||||||
|
|
||||||
stream := cipher.NewCFBDecrypter(block, iv)
|
stream := cipher.NewCFBDecrypter(block, iv)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
|
"html"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CheckMailSettings() *model.AppError {
|
func CheckMailSettings() *model.AppError {
|
||||||
@@ -84,7 +85,7 @@ func SendMail(to, subject, body string) *model.AppError {
|
|||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
headers["From"] = fromMail.String()
|
headers["From"] = fromMail.String()
|
||||||
headers["To"] = toMail.String()
|
headers["To"] = toMail.String()
|
||||||
headers["Subject"] = subject
|
headers["Subject"] = html.UnescapeString(subject)
|
||||||
headers["MIME-version"] = "1.0"
|
headers["MIME-version"] = "1.0"
|
||||||
headers["Content-Type"] = "text/html"
|
headers["Content-Type"] = "text/html"
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,8 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
|||||||
var Constants = require('../utils/constants.jsx');
|
var Constants = require('../utils/constants.jsx');
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
function getExtraInfoStateFromStores() {
|
|
||||||
return {
|
|
||||||
extra_info: ChannelStore.getCurrentExtraInfo()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var ExtraMembers = React.createClass({
|
var ExtraMembers = React.createClass({
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
ChannelStore.addExtraInfoChangeListener(this._onChange);
|
|
||||||
ChannelStore.addChangeListener(this._onChange);
|
|
||||||
|
|
||||||
var originalLeave = $.fn.popover.Constructor.prototype.leave;
|
var originalLeave = $.fn.popover.Constructor.prototype.leave;
|
||||||
$.fn.popover.Constructor.prototype.leave = function(obj) {
|
$.fn.popover.Constructor.prototype.leave = function(obj) {
|
||||||
var self = obj instanceof this.constructor ? obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type);
|
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() {
|
render: function() {
|
||||||
var count = this.state.extra_info.members.length == 0 ? "-" : this.state.extra_info.members.length;
|
var count = this.props.members.length == 0 ? "-" : this.props.members.length;
|
||||||
count = this.state.extra_info.members.length > 19 ? "20+" : count;
|
count = this.props.members.length > 19 ? "20+" : count;
|
||||||
var data_content = "";
|
var data_content = "";
|
||||||
|
var sortedMembers = this.props.members;
|
||||||
|
|
||||||
this.state.extra_info.members.forEach(function(m) {
|
if(sortedMembers) {
|
||||||
data_content += "<div style='white-space: nowrap'>" + m.username + "</div>";
|
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 (
|
return (
|
||||||
<div style={{"cursor" : "pointer"}} id="member_popover" data-toggle="popover" data-content={data_content} data-original-title="Members" >
|
<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>
|
<a href="#"><strong className="heading">{channelTitle}</strong></a>
|
||||||
}
|
}
|
||||||
</th>
|
</th>
|
||||||
<th><ExtraMembers channelId={this.state.channel.id} /></th>
|
<th><ExtraMembers members={this.state.users} channelId={this.state.channel.id} /></th>
|
||||||
{ searchForm }
|
{ searchForm }
|
||||||
<th>
|
<th>
|
||||||
<div className="dropdown" style={{"marginLeft":"5px", "marginRight":"10px"}}>
|
<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>
|
<h4 className="modal-title" id="myModalLabel">{channel.display_name}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<p><strong>Channel Name: </strong>{channel.display_name}</p>
|
<div className="row form-group">
|
||||||
<p><strong>Channel Handle: </strong>{channel.name}</p>
|
<div className="col-sm-3 info__label">Channel Name: </div>
|
||||||
<p><strong>Channel ID: </strong>{channel.id}</p>
|
<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>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// 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 utils = require('../utils/utils.jsx');
|
||||||
var client = require('../utils/client.jsx');
|
var client = require('../utils/client.jsx');
|
||||||
@@ -9,26 +11,50 @@ var ChannelStore = require('../stores/channel_store.jsx');
|
|||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
ChannelStore.addChangeListener(this._onChange);
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||||
var button = e.relatedTarget;
|
var button = e.relatedTarget;
|
||||||
var channel_id = button.dataset.channelid;
|
var channel_id = button.dataset.channelid;
|
||||||
|
|
||||||
var notifyLevel = ChannelStore.getMember(channel_id).notify_level;
|
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() {
|
componentWillUnmount: function() {
|
||||||
return { notify_level: "", title: "", channel_id: "" };
|
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 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 = {};
|
var data = {};
|
||||||
data["channel_id"] = channel_id;
|
data["channel_id"] = channel_id;
|
||||||
data["user_id"] = UserStore.getCurrentId();
|
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;
|
if (!data["notify_level"] || data["notify_level"].length === 0) return;
|
||||||
|
|
||||||
@@ -37,7 +63,7 @@ module.exports = React.createClass({
|
|||||||
var member = ChannelStore.getMember(channel_id);
|
var member = ChannelStore.getMember(channel_id);
|
||||||
member.notify_level = notify_level;
|
member.notify_level = notify_level;
|
||||||
ChannelStore.setChannelMember(member);
|
ChannelStore.setChannelMember(member);
|
||||||
$(this.refs.modal.getDOMNode()).modal('hide');
|
this.updateSection("");
|
||||||
}.bind(this),
|
}.bind(this),
|
||||||
function(err) {
|
function(err) {
|
||||||
this.setState({ server_error: err.message });
|
this.setState({ server_error: err.message });
|
||||||
@@ -45,42 +71,138 @@ module.exports = React.createClass({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
handleRadioClick: function(notifyLevel) {
|
handleRadioClick: function(notifyLevel) {
|
||||||
this.setState({ notify_level: notifyLevel });
|
this.setState({ notify_level: notifyLevel, quiet_mode: false });
|
||||||
this.refs.modal.getDOMNode().focus();
|
this.refs.modal.getDOMNode().focus();
|
||||||
},
|
},
|
||||||
handleQuietToggle: function() {
|
handleQuietToggle: function(quietMode) {
|
||||||
if (this.state.notify_level === "quiet") {
|
this.setState({ notify_level: "none", quiet_mode: quietMode });
|
||||||
this.setState({ notify_level: "none" });
|
this.refs.modal.getDOMNode().focus();
|
||||||
this.refs.modal.getDOMNode().focus();
|
|
||||||
} else {
|
|
||||||
this.setState({ notify_level: "quiet" });
|
|
||||||
this.refs.modal.getDOMNode().focus();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
render: function() {
|
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 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 self = this;
|
||||||
var mentionActive = "";
|
|
||||||
var noneActive = "";
|
|
||||||
var quietActive = "";
|
|
||||||
var desktopHidden = "";
|
|
||||||
|
|
||||||
if (this.state.notify_level === "quiet") {
|
var desktopSection;
|
||||||
desktopHidden = "hidden";
|
if (this.state.activeSection === 'desktop') {
|
||||||
quietActive = "active";
|
var notifyActive = [false, false, false];
|
||||||
} else if (this.state.notify_level === "mention") {
|
if (this.state.notify_level === "mention") {
|
||||||
mentionActive = "active";
|
notifyActive[1] = true;
|
||||||
} else if (this.state.notify_level === "none") {
|
} else if (this.state.notify_level === "all") {
|
||||||
noneActive = "active";
|
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 {
|
} 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;
|
var self = this;
|
||||||
return (
|
return (
|
||||||
<div className="modal fade" id="channel_notifications" ref="modal" tabIndex="-1" role="dialog" aria-hidden="true">
|
<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-content">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<button type="button" className="close" data-dismiss="modal">
|
<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>
|
<h4 className="modal-title">{"Notification Preferences for " + this.state.title}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<div className={desktopHidden}>
|
<div className="settings-table">
|
||||||
<span>Desktop Notifications</span>
|
<div className="settings-content">
|
||||||
<br/>
|
<div ref="wrapper" className="user-settings">
|
||||||
<div className="btn-group" data-toggle="buttons-radio">
|
<br/>
|
||||||
<button className={"btn btn-default "+allActive} onClick={function(){self.handleRadioClick("all")}}>Any activity (default)</button>
|
<div className="divider-dark first"/>
|
||||||
<button className={"btn btn-default "+mentionActive} onClick={function(){self.handleRadioClick("mention")}}>Mentions of my name</button>
|
{desktopSection}
|
||||||
<button className={"btn btn-default "+noneActive} onClick={function(){self.handleRadioClick("none")}}>Nothing</button>
|
<div className="divider-light"/>
|
||||||
|
{quietSection}
|
||||||
|
<div className="divider-dark"/>
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{ server_error }
|
{ server_error }
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
|
||||||
<button type="button" className="btn btn-primary" onClick={this.handleUpdate}>Done</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -112,13 +112,28 @@ module.exports = React.createClass({
|
|||||||
return { messageText: '', uploadsInProgress: 0, previews: [], submitting: false };
|
return { messageText: '', uploadsInProgress: 0, previews: [], submitting: false };
|
||||||
},
|
},
|
||||||
setUploads: function(val) {
|
setUploads: function(val) {
|
||||||
var num = this.state.uploadsInProgress + val;
|
var oldInProgress = this.state.uploadsInProgress
|
||||||
this.setState({uploadsInProgress: num});
|
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() {
|
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 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 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/>;
|
var preview = <div/>;
|
||||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
|
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
|
||||||
@@ -129,13 +144,6 @@ module.exports = React.createClass({
|
|||||||
uploadsInProgress={this.state.uploadsInProgress} />
|
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 (
|
return (
|
||||||
<form onSubmit={this.handleSubmit}>
|
<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} />
|
<input type="button" className="btn btn-primary comment-btn pull-right" value="Add Comment" onClick={this.handleSubmit} />
|
||||||
{ post_error }
|
{ post_error }
|
||||||
{ server_error }
|
{ server_error }
|
||||||
{ limit_previews }
|
{ limit_error }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ preview }
|
{ preview }
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ module.exports = React.createClass({
|
|||||||
false,
|
false,
|
||||||
function(data) {
|
function(data) {
|
||||||
PostStore.storeDraft(data.channel_id, user_id, null);
|
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) {
|
if (data.goto_location.length > 0) {
|
||||||
window.location.href = data.goto_location;
|
window.location.href = data.goto_location;
|
||||||
@@ -71,7 +71,7 @@ module.exports = React.createClass({
|
|||||||
client.createPost(post, ChannelStore.getCurrent(),
|
client.createPost(post, ChannelStore.getCurrent(),
|
||||||
function(data) {
|
function(data) {
|
||||||
PostStore.storeDraft(data.channel_id, data.user_id, null);
|
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();
|
this.resizePostHolder();
|
||||||
AsyncClient.getPosts(true);
|
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 };
|
return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText };
|
||||||
},
|
},
|
||||||
setUploads: function(val) {
|
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();
|
var draft = PostStore.getCurrentDraft();
|
||||||
if (!draft) {
|
if (!draft) {
|
||||||
draft = {}
|
draft = {}
|
||||||
draft['message'] = '';
|
draft['message'] = '';
|
||||||
draft['previews'] = [];
|
draft['previews'] = [];
|
||||||
}
|
}
|
||||||
draft['uploadsInProgress'] = num;
|
draft['uploadsInProgress'] = newInProgress;
|
||||||
PostStore.storeCurrentDraft(draft);
|
PostStore.storeCurrentDraft(draft);
|
||||||
this.setState({uploadsInProgress: num});
|
this.setState({uploadsInProgress: newInProgress});
|
||||||
|
|
||||||
|
return numToUpload;
|
||||||
},
|
},
|
||||||
render: function() {
|
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 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 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/>;
|
var preview = <div/>;
|
||||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
|
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
|
||||||
@@ -232,13 +247,6 @@ module.exports = React.createClass({
|
|||||||
uploadsInProgress={this.state.uploadsInProgress} />
|
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 (
|
return (
|
||||||
<form id="create_post" ref="topDiv" role="form" onSubmit={this.handleSubmit}>
|
<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'}>
|
<div className={post_error ? 'post-create-footer has-error' : 'post-create-footer'}>
|
||||||
{ post_error }
|
{ post_error }
|
||||||
{ server_error }
|
{ server_error }
|
||||||
{ limit_previews }
|
{ limit_error }
|
||||||
{ preview }
|
{ preview }
|
||||||
<MsgTyping channelId={this.state.channel_id} parentId=""/>
|
<MsgTyping channelId={this.state.channel_id} parentId=""/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ var Constants = require('../utils/constants.jsx');
|
|||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
handleRemove: function(e) {
|
handleRemove: function(e) {
|
||||||
var previewDiv = e.target.parentNode.parentNode;
|
var previewDiv = e.target.parentNode.parentNode;
|
||||||
this.props.onRemove(previewDiv.dataset.filename);
|
this.props.onRemove(previewDiv.getAttribute('data-filename'));
|
||||||
},
|
},
|
||||||
render: function() {
|
render: function() {
|
||||||
var previews = [];
|
var previews = [];
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
this.props.onUploadError(null);
|
this.props.onUploadError(null);
|
||||||
|
|
||||||
//This looks redundant, but must be done this way due to
|
// This looks redundant, but must be done this way due to
|
||||||
//setState being an asynchronous call
|
// setState being an asynchronous call
|
||||||
var numFiles = 0;
|
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) {
|
if (files[i].size <= Constants.MAX_FILE_SIZE) {
|
||||||
numFiles++;
|
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) {
|
if (files[i].size > Constants.MAX_FILE_SIZE) {
|
||||||
this.props.onUploadError("Files must be no more than " + Constants.MAX_FILE_SIZE/1000000 + " MB");
|
this.props.onUploadError("Files must be no more than " + Constants.MAX_FILE_SIZE/1000000 + " MB");
|
||||||
continue;
|
continue;
|
||||||
@@ -70,8 +70,8 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
self.props.onUploadError(null);
|
self.props.onUploadError(null);
|
||||||
|
|
||||||
//This looks redundant, but must be done this way due to
|
// This looks redundant, but must be done this way due to
|
||||||
//setState being an asynchronous call
|
// setState being an asynchronous call
|
||||||
var items = e.clipboardData.items;
|
var items = e.clipboardData.items;
|
||||||
var numItems = 0;
|
var numItems = 0;
|
||||||
if (items) {
|
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) {
|
if (items[i].type.indexOf("image") !== -1) {
|
||||||
var file = items[i].getAsFile();
|
var file = items[i].getAsFile();
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ module.exports = React.createClass({
|
|||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ var FindTeamDomain = React.createClass({
|
|||||||
window.location.href = window.location.protocol + "//" + domain + "." + utils.getDomainWithOutSub();
|
window.location.href = window.location.protocol + "//" + domain + "." + utils.getDomainWithOutSub();
|
||||||
}
|
}
|
||||||
else {
|
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);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}.bind(this),
|
}.bind(this),
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ module.exports = React.createClass({
|
|||||||
this.props.handleClick(this.props.username);
|
this.props.handleClick(this.props.username);
|
||||||
},
|
},
|
||||||
render: function() {
|
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 (
|
return (
|
||||||
<div className="mentions-name" onClick={this.handleClick}>
|
<div className="mentions-name" onClick={this.handleClick}>
|
||||||
<img className="pull-left mention-img" src={"/api/v1/users/" + this.props.id + "/image"}/>
|
<div className="pull-left">{icon}</div>
|
||||||
<span>@{this.props.username}</span><span style={{'color':'grey', 'marginLeft':'10px'}}>{this.props.name}</span>
|
<div className="pull-left mention-align"><span>@{this.props.username}</span><span className="mention-fullname">{this.props.secondary_text}</span></div>
|
||||||
</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() {
|
componentWillUnmount: function() {
|
||||||
PostStore.removeMentionDataChangeListener(this._onChange);
|
PostStore.removeMentionDataChangeListener(this._onChange);
|
||||||
@@ -74,6 +79,18 @@ module.exports = React.createClass({
|
|||||||
users.push(profiles[id]);
|
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) {
|
users.sort(function(a,b) {
|
||||||
if (a.username < b.username) return -1;
|
if (a.username < b.username) return -1;
|
||||||
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(' ');
|
var splitName = users[i].full_name.split(' ');
|
||||||
firstName = splitName[0].toLowerCase();
|
firstName = splitName[0].toLowerCase();
|
||||||
lastName = splitName.length > 1 ? splitName[splitName.length-1].toLowerCase() : "";
|
lastName = splitName.length > 1 ? splitName[splitName.length-1].toLowerCase() : "";
|
||||||
|
users[i].secondary_text = users[i].full_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstName.lastIndexOf(mentionText,0) === 0
|
if (firstName.lastIndexOf(mentionText,0) === 0
|
||||||
@@ -99,7 +117,7 @@ module.exports = React.createClass({
|
|||||||
<Mention
|
<Mention
|
||||||
ref={'mention' + index}
|
ref={'mention' + index}
|
||||||
username={users[i].username}
|
username={users[i].username}
|
||||||
name={users[i].full_name}
|
secondary_text={users[i].secondary_text}
|
||||||
id={users[i].id}
|
id={users[i].id}
|
||||||
handleClick={this.handleClick} />
|
handleClick={this.handleClick} />
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ module.exports = React.createClass({
|
|||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</button>
|
</button>
|
||||||
<h4 className="modal-title">More Direct Messages</h4>
|
<h4 className="modal-title">More Private Messages</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<ul className="nav nav-pills nav-stacked">
|
<ul className="nav nav-pills nav-stacked">
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ var PostBody = require('./post_body.jsx');
|
|||||||
var PostInfo = require('./post_info.jsx');
|
var PostInfo = require('./post_info.jsx');
|
||||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||||
var Constants = require('../utils/constants.jsx');
|
var Constants = require('../utils/constants.jsx');
|
||||||
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
$('.edit-modal').on('show.bs.modal', function () {
|
$('.modal').on('show.bs.modal', function () {
|
||||||
$('.edit-modal .edit-modal-body').css('overflow-y', 'auto');
|
$('.modal-body').css('overflow-y', 'auto');
|
||||||
$('.edit-modal .edit-modal-body').css('max-height', $(window).height() * 0.7);
|
$('.modal-body').css('max-height', $(window).height() * 0.7);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleCommentClick: function(e) {
|
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;
|
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";
|
rootUser = "same--root";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -64,13 +65,18 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
var postType = "";
|
var postType = "";
|
||||||
if(type != "Post"){
|
if (type != "Post"){
|
||||||
postType = "post--comment";
|
postType = "post--comment";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentUserCss = "";
|
||||||
|
if (UserStore.getCurrentId() === post.user_id) {
|
||||||
|
currentUserCss = "current--user";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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 ?
|
{ !this.props.hideProfilePic ?
|
||||||
<div className="post-profile-img__container">
|
<div className="post-profile-img__container">
|
||||||
<img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image"} height="36" width="36" />
|
<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]);
|
images.push(filenames[i]);
|
||||||
} else if (i < Constants.MAX_DISPLAY_FILES) {
|
} else if (i < Constants.MAX_DISPLAY_FILES) {
|
||||||
postFiles.push(
|
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}>
|
<a href={fileInfo.path+"."+fileInfo.ext} download={fileInfo.name+"."+fileInfo.ext}>
|
||||||
<div className={"file-icon "+utils.getIconClassName(type)}/>
|
<div className={"file-icon "+utils.getIconClassName(type)}/>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
render: function() {
|
render: function() {
|
||||||
var order = [];
|
var order = [];
|
||||||
var posts = {};
|
var posts;
|
||||||
|
|
||||||
var last_viewed = Number.MAX_VALUE;
|
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) {
|
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>;
|
more_messages = <a ref="loadmore" className="more-messages-text theme" href="#" onClick={this.getMorePosts}>Load more messages</a>;
|
||||||
} else if (channel.type === 'D') {
|
} else if (channel.type === 'D') {
|
||||||
var userIds = channel.name.split('__');
|
var teammate = utils.getDirectTeammate(channel.id)
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (teammate) {
|
if (teammate) {
|
||||||
var teammate_name = teammate.full_name.length > 0 ? teammate.full_name : teammate.username;
|
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">
|
<div className="channel-intro-profile">
|
||||||
<strong><UserProfile userId={teammate.id} /></strong>
|
<strong><UserProfile userId={teammate.id} /></strong>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
more_messages = (
|
more_messages = (
|
||||||
<div className="channel-intro">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -356,6 +350,7 @@ module.exports = React.createClass({
|
|||||||
var ui_name = channel.display_name
|
var ui_name = channel.display_name
|
||||||
var members = ChannelStore.getCurrentExtraInfo().members;
|
var members = ChannelStore.getCurrentExtraInfo().members;
|
||||||
var creator_name = "";
|
var creator_name = "";
|
||||||
|
var userStyle = { color: UserStore.getCurrentUser().props.theme }
|
||||||
|
|
||||||
for (var i = 0; i < members.length; i++) {
|
for (var i = 0; i < members.length; i++) {
|
||||||
if (members[i].roles.indexOf('admin') > -1) {
|
if (members[i].roles.indexOf('admin') > -1) {
|
||||||
@@ -382,8 +377,18 @@ module.exports = React.createClass({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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 {
|
} else {
|
||||||
var userStyle = { color: UserStore.getCurrentUser().props.theme }
|
|
||||||
var ui_type = channel.type === 'P' ? "private group" : "channel";
|
var ui_type = channel.type === 'P' ? "private group" : "channel";
|
||||||
more_messages = (
|
more_messages = (
|
||||||
<div className="channel-intro">
|
<div className="channel-intro">
|
||||||
@@ -403,55 +408,70 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
var postCtls = [];
|
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--) {
|
if (posts != undefined) {
|
||||||
var post = posts[order[i]];
|
var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date();
|
||||||
var parentPost;
|
var currentPostDay = new Date();
|
||||||
|
|
||||||
if (post.parent_id) {
|
for (var i = order.length-1; i >= 0; i--) {
|
||||||
parentPost = posts[post.parent_id];
|
var post = posts[order[i]];
|
||||||
} else {
|
var parentPost;
|
||||||
parentPost = null;
|
|
||||||
|
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" : "";
|
else {
|
||||||
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;
|
postCtls.push(
|
||||||
|
<div ref="loadingscreen" className="loading-screen">
|
||||||
// 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
|
<div className="loading__content">
|
||||||
var hideProfilePic = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && posts[order[i+1]].root_id === '' && post.root_id === '';
|
<h3>Loading</h3>
|
||||||
|
<div id="round_1" className="round"></div>
|
||||||
// check if it's the last comment in a consecutive string of comments on the same post
|
<div id="round_2" className="round"></div>
|
||||||
var isLastComment = false;
|
<div id="round_3" className="round"></div>
|
||||||
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>
|
</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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -68,9 +68,14 @@ RootPost = React.createClass({
|
|||||||
var filenames = this.props.post.filenames;
|
var filenames = this.props.post.filenames;
|
||||||
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
|
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
|
||||||
|
|
||||||
var type = "Post"
|
var type = "Post";
|
||||||
if (this.props.post.root_id.length > 0) {
|
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) {
|
if (filenames) {
|
||||||
@@ -84,7 +89,7 @@ RootPost = React.createClass({
|
|||||||
if (fileSplit.length < 2) continue;
|
if (fileSplit.length < 2) continue;
|
||||||
|
|
||||||
var ext = fileSplit[fileSplit.length-1];
|
var ext = fileSplit[fileSplit.length-1];
|
||||||
fileSplit.splice(fileSplit.length-1,1)
|
fileSplit.splice(fileSplit.length-1,1);
|
||||||
var filePath = fileSplit.join('.');
|
var filePath = fileSplit.join('.');
|
||||||
var filename = filePath.split('/')[filePath.split('/').length-1];
|
var filename = filePath.split('/')[filePath.split('/').length-1];
|
||||||
|
|
||||||
@@ -111,7 +116,7 @@ RootPost = React.createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="post post--root">
|
<div className={"post post--root " + currentUserCss}>
|
||||||
<div className="post-profile-img__container">
|
<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" />
|
<img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" />
|
||||||
</div>
|
</div>
|
||||||
@@ -170,6 +175,11 @@ CommentPost = React.createClass({
|
|||||||
|
|
||||||
var commentClass = "post";
|
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 postImageModalId = "rhs_comment_view_image_modal_" + this.props.post.id;
|
||||||
var filenames = this.props.post.filenames;
|
var filenames = this.props.post.filenames;
|
||||||
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
|
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
|
||||||
@@ -219,7 +229,7 @@ CommentPost = React.createClass({
|
|||||||
var message = utils.textToJsx(this.props.post.message);
|
var message = utils.textToJsx(this.props.post.message);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={commentClass}>
|
<div className={commentClass + " " + currentUserCss}>
|
||||||
<div className="post-profile-img__container">
|
<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" />
|
<img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ SearchItem = React.createClass({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
client.getPost(
|
client.getPost(
|
||||||
this.props.post.channel_id,
|
this.props.post.channel_id,
|
||||||
this.props.post.id,
|
this.props.post.id,
|
||||||
@@ -64,6 +65,11 @@ SearchItem = React.createClass({
|
|||||||
dispatchError(err, "getPost");
|
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() {
|
render: function() {
|
||||||
|
|
||||||
@@ -73,7 +79,7 @@ SearchItem = React.createClass({
|
|||||||
|
|
||||||
if (channel) {
|
if (channel) {
|
||||||
if (channel.type === 'D') {
|
if (channel.type === 'D') {
|
||||||
channelName = "Direct Message";
|
channelName = "Private Message";
|
||||||
} else {
|
} else {
|
||||||
channelName = channel.display_name;
|
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-12 section-title">{this.props.title}</li>
|
||||||
<li className="col-sm-9 col-sm-offset-3">
|
<li className="col-sm-9 col-sm-offset-3">
|
||||||
<ul className="setting-list">
|
<ul className="setting-list">
|
||||||
<li className="row setting-list-item form-group">
|
<li className="setting-list-item">
|
||||||
{inputs}
|
{inputs}
|
||||||
</li>
|
</li>
|
||||||
<li className="setting-list-item">
|
<li className="setting-list-item">
|
||||||
|
|||||||
@@ -269,13 +269,8 @@ var SidebarLoggedIn = React.createClass({
|
|||||||
var channel = ChannelStore.getCurrent();
|
var channel = ChannelStore.getCurrent();
|
||||||
if (channel) {
|
if (channel) {
|
||||||
if (channel.type === 'D') {
|
if (channel.type === 'D') {
|
||||||
userIds = channel.name.split('__');
|
var teammate_username = utils.getDirectTeammate(channel.id).username
|
||||||
if (userIds.length < 2) return;
|
document.title = teammate_username + " " + document.title.substring(document.title.lastIndexOf("-"));
|
||||||
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("-"));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
document.title = channel.display_name + " " + document.title.substring(document.title.lastIndexOf("-"))
|
document.title = channel.display_name + " " + document.title.substring(document.title.lastIndexOf("-"))
|
||||||
}
|
}
|
||||||
@@ -414,7 +409,7 @@ var SidebarLoggedIn = React.createClass({
|
|||||||
{privateChannelItems}
|
{privateChannelItems}
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="nav nav-pills nav-stacked">
|
<ul className="nav nav-pills nav-stacked">
|
||||||
<li><h4>Direct Messages</h4></li>
|
<li><h4>Private Messages</h4></li>
|
||||||
{directMessageItems}
|
{directMessageItems}
|
||||||
{ this.state.hideDirectChannels.length > 0 ?
|
{ 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>
|
<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">
|
<div className="nav-pills__container">
|
||||||
<ul className="nav nav-pills nav-stacked">
|
<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>
|
<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 }
|
{ invite_link }
|
||||||
{ team_link }
|
{ team_link }
|
||||||
{ manage_link }
|
{ manage_link }
|
||||||
|
|||||||
@@ -164,7 +164,9 @@ TeamUrlPage = React.createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cleaned_name = utils.cleanUpUrlable(name);
|
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"});
|
this.setState({name_error: "Must be lowercase alphanumeric characters"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -337,7 +339,7 @@ EmailItem = React.createClass({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (email === teamEmail) {
|
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);
|
this.setState(this.state);
|
||||||
return false;
|
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[0]} onClick={function(){self.handleValetRadio("true")}}>On</button>
|
||||||
<button className={"btn btn-default "+valetActive[1]} onClick={function(){self.handleValetRadio("false")}}>Off</button>
|
<button className={"btn btn-default "+valetActive[1]} onClick={function(){self.handleValetRadio("false")}}>Off</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
valetSection = (
|
valetSection = (
|
||||||
<SettingItemMax
|
<SettingItemMax
|
||||||
title="Valet"
|
title="Valet (Preview - EXPERTS ONLY)"
|
||||||
inputs={inputs}
|
inputs={inputs}
|
||||||
submit={this.submitValetFeature}
|
submit={this.submitValetFeature}
|
||||||
server_error={server_error}
|
server_error={server_error}
|
||||||
@@ -102,7 +103,7 @@ var FeatureTab = React.createClass({
|
|||||||
|
|
||||||
valetSection = (
|
valetSection = (
|
||||||
<SettingItemMin
|
<SettingItemMin
|
||||||
title="Valet"
|
title="Valet (Preview - EXPERTS ONLY)"
|
||||||
describe={describe}
|
describe={describe}
|
||||||
updateSection={function(){self.props.updateSection("valet");}}
|
updateSection={function(){self.props.updateSection("valet");}}
|
||||||
/>
|
/>
|
||||||
@@ -113,7 +114,7 @@ var FeatureTab = React.createClass({
|
|||||||
<div>
|
<div>
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
<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>
|
||||||
<div ref="wrapper" className="user-settings">
|
<div ref="wrapper" className="user-settings">
|
||||||
<h3 className="tab-header">Feature Settings</h3>
|
<h3 className="tab-header">Feature Settings</h3>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ module.exports = React.createClass({
|
|||||||
updateTab={this.updateTab}
|
updateTab={this.updateTab}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-content">
|
<div className="settings-content minimize-settings">
|
||||||
<TeamSettings
|
<TeamSettings
|
||||||
activeTab={this.state.active_tab}
|
activeTab={this.state.active_tab}
|
||||||
activeSection={this.state.active_section}
|
activeSection={this.state.active_section}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ module.exports = React.createClass({
|
|||||||
var mentions = [];
|
var mentions = [];
|
||||||
for (var i = 0; i < matches.length; i++) {
|
for (var i = 0; i < matches.length; i++) {
|
||||||
var m = matches[i].substring(1,matches[i].length).trim();
|
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);
|
mentions.push(m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ function getStateFromStores(userId) {
|
|||||||
|
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
return { profile: { id: "0", username: "..."} };
|
return { profile: { id: "0", username: "..."} };
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return { profile: profile };
|
return { profile: profile };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,12 +53,11 @@ module.exports = React.createClass({
|
|||||||
var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username;
|
var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username;
|
||||||
|
|
||||||
|
|
||||||
var data_content = ""
|
var data_content = "<img style='margin: 10px' src='/api/v1/users/" + this.state.profile.id + "/image' height='128' width='128' />";
|
||||||
data_content += "<img style='margin: 10px' src='/api/v1/users/" + this.state.profile.id + "/image' height='128' width='128' />"
|
|
||||||
if (!config.ShowEmail) {
|
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 {
|
} 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 (
|
return (
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ function getNotificationsStateFromStores() {
|
|||||||
var mention_key = false;
|
var mention_key = false;
|
||||||
var custom_keys = "";
|
var custom_keys = "";
|
||||||
var first_name_key = false;
|
var first_name_key = false;
|
||||||
|
var all_key = false;
|
||||||
|
var channel_key = false;
|
||||||
|
|
||||||
if (!user.notify_props) {
|
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.mention_keys !== undefined) {
|
if (user.notify_props.mention_keys !== undefined) {
|
||||||
var keys = user.notify_props.mention_keys.split(',');
|
var keys = user.notify_props.mention_keys.split(',');
|
||||||
|
|
||||||
@@ -48,9 +47,17 @@ function getNotificationsStateFromStores() {
|
|||||||
if (user.notify_props.first_name !== undefined) {
|
if (user.notify_props.first_name !== undefined) {
|
||||||
first_name_key = user.notify_props.first_name === "true";
|
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["mention_keys"] = string_keys;
|
||||||
data["first_name"] = this.state.first_name_key ? "true" : "false";
|
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,
|
client.updateUserNotifyProps(data,
|
||||||
function(data) {
|
function(data) {
|
||||||
@@ -120,6 +129,12 @@ var NotificationsTab = React.createClass({
|
|||||||
updateFirstNameKey: function(val) {
|
updateFirstNameKey: function(val) {
|
||||||
this.setState({ first_name_key: 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() {
|
updateCustomMentionKeys: function() {
|
||||||
var checked = this.refs.customcheck.getDOMNode().checked;
|
var checked = this.refs.customcheck.getDOMNode().checked;
|
||||||
|
|
||||||
@@ -155,7 +170,7 @@ var NotificationsTab = React.createClass({
|
|||||||
var inputs = [];
|
var inputs = [];
|
||||||
|
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div className="col-sm-12">
|
<div>
|
||||||
<div className="radio">
|
<div className="radio">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" checked={notifyActive[0]} onClick={function(){self.handleNotifyRadio("all")}}>For all activity</input>
|
<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>
|
||||||
<div className="radio">
|
<div className="radio">
|
||||||
<label>
|
<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>
|
</label>
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,7 +203,7 @@ var NotificationsTab = React.createClass({
|
|||||||
} else {
|
} else {
|
||||||
var describe = "";
|
var describe = "";
|
||||||
if (this.state.notify_level === "mention") {
|
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") {
|
} else if (this.state.notify_level === "none") {
|
||||||
describe = "Never";
|
describe = "Never";
|
||||||
} else {
|
} else {
|
||||||
@@ -216,7 +231,7 @@ var NotificationsTab = React.createClass({
|
|||||||
var inputs = [];
|
var inputs = [];
|
||||||
|
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div className="col-sm-12">
|
<div>
|
||||||
<div className="btn-group" data-toggle="buttons-radio">
|
<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[0]} onClick={function(){self.handleSoundRadio("true")}}>On</button>
|
||||||
<button className={"btn btn-default "+soundActive[1]} onClick={function(){self.handleSoundRadio("false")}}>Off</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 = [];
|
var inputs = [];
|
||||||
|
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div className="col-sm-12">
|
<div>
|
||||||
<div className="btn-group" data-toggle="buttons-radio">
|
<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[0]} onClick={function(){self.handleEmailRadio("true")}}>On</button>
|
||||||
<button className={"btn btn-default "+emailActive[1]} onClick={function(){self.handleEmailRadio("false")}}>Off</button>
|
<button className={"btn btn-default "+emailActive[1]} onClick={function(){self.handleEmailRadio("false")}}>Off</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -309,7 +324,7 @@ var NotificationsTab = React.createClass({
|
|||||||
|
|
||||||
if (first_name != "") {
|
if (first_name != "") {
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div className="col-sm-12">
|
<div>
|
||||||
<div className="checkbox">
|
<div className="checkbox">
|
||||||
<label>
|
<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>
|
<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(
|
inputs.push(
|
||||||
<div className="col-sm-12">
|
<div>
|
||||||
<div className="checkbox">
|
<div className="checkbox">
|
||||||
<label>
|
<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>
|
<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(
|
inputs.push(
|
||||||
<div className="col-sm-12">
|
<div>
|
||||||
<div className="checkbox">
|
<div className="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" checked={this.state.mention_key} onChange={function(e){self.updateMentionKey(e.target.checked);}}>{'Your username mentioned "@' + user.username + '"'}</input>
|
<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(
|
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">
|
<div className="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input ref="customcheck" type="checkbox" checked={this.state.custom_keys_checked} onChange={this.updateCustomMentionKeys}>{'Other non-case sensitive words, separated by commas:'}</input>
|
<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.username_key) keys.push(this.props.user.username);
|
||||||
if (this.state.mention_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(','));
|
if (this.state.custom_keys.length > 0) keys = keys.concat(this.state.custom_keys.split(','));
|
||||||
|
|
||||||
var describe = "";
|
var describe = "";
|
||||||
@@ -622,7 +659,7 @@ var SecurityTab = React.createClass({
|
|||||||
var inputs = [];
|
var inputs = [];
|
||||||
|
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div>
|
<div className="form-group">
|
||||||
<label className="col-sm-5 control-label">Current Password</label>
|
<label className="col-sm-5 control-label">Current Password</label>
|
||||||
<div className="col-sm-7">
|
<div className="col-sm-7">
|
||||||
<input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/>
|
<input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/>
|
||||||
@@ -630,7 +667,7 @@ var SecurityTab = React.createClass({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div>
|
<div className="form-group">
|
||||||
<label className="col-sm-5 control-label">New Password</label>
|
<label className="col-sm-5 control-label">New Password</label>
|
||||||
<div className="col-sm-7">
|
<div className="col-sm-7">
|
||||||
<input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/>
|
<input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/>
|
||||||
@@ -638,7 +675,7 @@ var SecurityTab = React.createClass({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div>
|
<div className="form-group">
|
||||||
<label className="col-sm-5 control-label">Retype New Password</label>
|
<label className="col-sm-5 control-label">Retype New Password</label>
|
||||||
<div className="col-sm-7">
|
<div className="col-sm-7">
|
||||||
<input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/>
|
<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.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 = new FormData();
|
||||||
formData.append('image', this.state.picture, this.state.picture.name);
|
formData.append('image', this.state.picture, this.state.picture.name);
|
||||||
|
|
||||||
@@ -802,11 +844,13 @@ var GeneralTab = React.createClass({
|
|||||||
updatePicture: function(e) {
|
updatePicture: function(e) {
|
||||||
if (e.target.files && e.target.files[0]) {
|
if (e.target.files && e.target.files[0]) {
|
||||||
this.setState({ picture: e.target.files[0] });
|
this.setState({ picture: e.target.files[0] });
|
||||||
|
|
||||||
|
this.submitActive = true;
|
||||||
|
this.setState({client_error:null})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({ picture: null });
|
this.setState({ picture: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.submitActive = true
|
|
||||||
},
|
},
|
||||||
updateSection: function(section) {
|
updateSection: function(section) {
|
||||||
this.setState({client_error:""})
|
this.setState({client_error:""})
|
||||||
@@ -837,7 +881,7 @@ var GeneralTab = React.createClass({
|
|||||||
var inputs = [];
|
var inputs = [];
|
||||||
|
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div>
|
<div className="form-group">
|
||||||
<label className="col-sm-5 control-label">First Name</label>
|
<label className="col-sm-5 control-label">First Name</label>
|
||||||
<div className="col-sm-7">
|
<div className="col-sm-7">
|
||||||
<input className="form-control" type="text" onChange={this.updateFirstName} value={this.state.first_name}/>
|
<input className="form-control" type="text" onChange={this.updateFirstName} value={this.state.first_name}/>
|
||||||
@@ -846,7 +890,7 @@ var GeneralTab = React.createClass({
|
|||||||
);
|
);
|
||||||
|
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div>
|
<div className="form-group">
|
||||||
<label className="col-sm-5 control-label">Last Name</label>
|
<label className="col-sm-5 control-label">Last Name</label>
|
||||||
<div className="col-sm-7">
|
<div className="col-sm-7">
|
||||||
<input className="form-control" type="text" onChange={this.updateLastName} value={this.state.last_name}/>
|
<input className="form-control" type="text" onChange={this.updateLastName} value={this.state.last_name}/>
|
||||||
@@ -879,7 +923,7 @@ var GeneralTab = React.createClass({
|
|||||||
var inputs = [];
|
var inputs = [];
|
||||||
|
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div>
|
<div className="form-group">
|
||||||
<label className="col-sm-5 control-label">{utils.isMobile() ? "": "Username"}</label>
|
<label className="col-sm-5 control-label">{utils.isMobile() ? "": "Username"}</label>
|
||||||
<div className="col-sm-7">
|
<div className="col-sm-7">
|
||||||
<input className="form-control" type="text" onChange={this.updateUsername} value={this.state.username}/>
|
<input className="form-control" type="text" onChange={this.updateUsername} value={this.state.username}/>
|
||||||
@@ -911,7 +955,7 @@ var GeneralTab = React.createClass({
|
|||||||
var inputs = [];
|
var inputs = [];
|
||||||
|
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div>
|
<div className="form-group">
|
||||||
<label className="col-sm-5 control-label">Primary Email</label>
|
<label className="col-sm-5 control-label">Primary Email</label>
|
||||||
<div className="col-sm-7">
|
<div className="col-sm-7">
|
||||||
<input className="form-control" type="text" onChange={this.updateEmail} value={this.state.email}/>
|
<input className="form-control" type="text" onChange={this.updateEmail} value={this.state.email}/>
|
||||||
@@ -947,7 +991,7 @@ var GeneralTab = React.createClass({
|
|||||||
submit={this.submitPicture}
|
submit={this.submitPicture}
|
||||||
src={"/api/v1/users/" + user.id + "/image"}
|
src={"/api/v1/users/" + user.id + "/image"}
|
||||||
server_error={server_error}
|
server_error={server_error}
|
||||||
client_error={email_error}
|
client_error={client_error}
|
||||||
updateSection={function(e){self.updateSection("");e.preventDefault();}}
|
updateSection={function(e){self.updateSection("");e.preventDefault();}}
|
||||||
picture={this.state.picture}
|
picture={this.state.picture}
|
||||||
pictureChange={this.updatePicture}
|
pictureChange={this.updatePicture}
|
||||||
@@ -1048,7 +1092,7 @@ var AppearanceTab = React.createClass({
|
|||||||
var inputs = [];
|
var inputs = [];
|
||||||
|
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<li className="row setting-list-item form-group">
|
<li className="setting-list-item">
|
||||||
<div className="btn-group" data-toggle="buttons-radio">
|
<div className="btn-group" data-toggle="buttons-radio">
|
||||||
{ theme_buttons }
|
{ theme_buttons }
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ module.exports = React.createClass({
|
|||||||
updateTab={this.updateTab}
|
updateTab={this.updateTab}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-content">
|
<div className="settings-content minimize-settings">
|
||||||
<UserSettings
|
<UserSettings
|
||||||
activeTab={this.state.active_tab}
|
activeTab={this.state.active_tab}
|
||||||
activeSection={this.state.active_section}
|
activeSection={this.state.active_section}
|
||||||
|
|||||||
@@ -240,6 +240,9 @@ var UserStore = assign({}, EventEmitter.prototype, {
|
|||||||
if (first.length > 0) keys.push(first);
|
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;
|
return keys;
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ module.exports = {
|
|||||||
SERVER_ACTION: null,
|
SERVER_ACTION: null,
|
||||||
VIEW_ACTION: null
|
VIEW_ACTION: null
|
||||||
}),
|
}),
|
||||||
|
SPECIAL_MENTIONS: ['all', 'channel'],
|
||||||
CHARACTER_LIMIT: 4000,
|
CHARACTER_LIMIT: 4000,
|
||||||
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png'],
|
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png'],
|
||||||
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac'],
|
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac'],
|
||||||
@@ -48,8 +49,10 @@ module.exports = {
|
|||||||
PATCH_TYPES: ['patch'],
|
PATCH_TYPES: ['patch'],
|
||||||
ICON_FROM_TYPE: {'audio': 'audio', 'video': 'video', 'spreadsheet': 'ppt', 'pdf': 'pdf', 'code': 'code' , 'word': 'word' , 'excel': 'excel' , 'patch': 'patch', 'other': 'generic'},
|
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_DISPLAY_FILES: 5,
|
||||||
|
MAX_UPLOAD_FILES: 5,
|
||||||
MAX_FILE_SIZE: 50000000, // 50 MB
|
MAX_FILE_SIZE: 50000000, // 50 MB
|
||||||
DEFAULT_CHANNEL: 'town-square',
|
DEFAULT_CHANNEL: 'town-square',
|
||||||
|
OFFTOPIC_CHANNEL: 'off-topic',
|
||||||
POST_CHUNK_SIZE: 60,
|
POST_CHUNK_SIZE: 60,
|
||||||
RESERVED_DOMAINS: [
|
RESERVED_DOMAINS: [
|
||||||
"www",
|
"www",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||||
|
var ChannelStore = require('../stores/channel_store.jsx')
|
||||||
var UserStore = require('../stores/user_store.jsx');
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
var Constants = require('../utils/constants.jsx');
|
var Constants = require('../utils/constants.jsx');
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
@@ -395,14 +396,10 @@ module.exports.textToJsx = function(text, options) {
|
|||||||
|
|
||||||
var inner = [];
|
var inner = [];
|
||||||
|
|
||||||
// Function specific regexes
|
// Function specific regex
|
||||||
var hashRegex = /^href="#[^"]+"|(#[A-Za-z]+[A-Za-z0-9_]*[A-Za-z0-9])$/g;
|
var hashRegex = /^href="#[^"]+"|(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$/g;
|
||||||
|
|
||||||
var implicitKeywords = {};
|
var implicitKeywords = UserStore.getCurrentMentionKeys();
|
||||||
var keywordArray = UserStore.getCurrentMentionKeys();
|
|
||||||
for (var i = 0; i < keywordArray.length; i++) {
|
|
||||||
implicitKeywords[keywordArray[i]] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lines = text.split("\n");
|
var lines = text.split("\n");
|
||||||
var urlMatcher = new LinkifyIt();
|
var urlMatcher = new LinkifyIt();
|
||||||
@@ -421,10 +418,13 @@ module.exports.textToJsx = function(text, options) {
|
|||||||
highlightSearchClass = " search-highlight";
|
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];
|
var name = explicitMention[1];
|
||||||
// do both a non-case sensitive and case senstive check
|
// 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 suffix = word.match(puncEndRegex);
|
||||||
var prefix = word.match(puncStartRegex);
|
var prefix = word.match(puncStartRegex);
|
||||||
@@ -446,7 +446,7 @@ module.exports.textToJsx = function(text, options) {
|
|||||||
} else if (trimWord.match(hashRegex)) {
|
} else if (trimWord.match(hashRegex)) {
|
||||||
var suffix = word.match(puncEndRegex);
|
var suffix = word.match(puncEndRegex);
|
||||||
var prefix = word.match(puncStartRegex);
|
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()) {
|
if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) {
|
||||||
highlightSearchClass = " search-highlight";
|
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>);
|
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 suffix = word.match(puncEndRegex);
|
||||||
var prefix = word.match(puncStartRegex);
|
var prefix = word.match(puncStartRegex);
|
||||||
|
|
||||||
@@ -727,6 +727,25 @@ module.exports.isComment = function(post) {
|
|||||||
return false;
|
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) {
|
Image.prototype.load = function(url, progressCallback) {
|
||||||
var thisImg = this;
|
var thisImg = this;
|
||||||
var xmlHTTP = new XMLHttpRequest();
|
var xmlHTTP = new XMLHttpRequest();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Lato', sans-serif;
|
font-family: 'Open Sans', sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
background: #e9e9e9;
|
background: #e9e9e9;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -11,21 +11,29 @@ body {
|
|||||||
&.white {
|
&.white {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
.inner__wrap {
|
.inner__wrap {
|
||||||
height: 100%;
|
> .row.content {
|
||||||
|
min-height: 100%;
|
||||||
|
margin-bottom: -89px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.row.content {
|
}
|
||||||
min-height: 100%;
|
.inner__wrap {
|
||||||
height: auto !important;
|
height: 100%;
|
||||||
|
> .row.main {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-bottom: -89px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> .container-fluid {
|
> .container-fluid {
|
||||||
@include clearfix;
|
@include clearfix;
|
||||||
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b, strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
@@ -142,7 +150,7 @@ div.theme {
|
|||||||
top: 0;
|
top: 0;
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
float:left;
|
float:left;
|
||||||
}
|
}
|
||||||
.channel-intro-title {
|
.channel-intro-title {
|
||||||
font-weight:bold;
|
font-weight:600;
|
||||||
}
|
}
|
||||||
.channel-intro-text {
|
.channel-intro-text {
|
||||||
margin-top:35px;
|
margin-top:35px;
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
}
|
}
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
li a {
|
li a {
|
||||||
padding: 3 20px;
|
padding: 3px 20px;
|
||||||
color: #555;
|
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;
|
background: $primary-color;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
padding-bottom: 1px;
|
padding-bottom: 2px;
|
||||||
@include border-radius(3px);
|
@include border-radius(3px);
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mentions--top {
|
.mentions--top {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index:99999;
|
z-index: 1060;
|
||||||
.mentions-box {
|
.mentions-box {
|
||||||
position:absolute;
|
position:absolute;
|
||||||
background-color:#fff;
|
background-color:#fff;
|
||||||
border:1px solid #ddd;
|
border: $border-gray;
|
||||||
overflow:scroll;
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
bottom:0;
|
bottom:0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,10 +27,10 @@
|
|||||||
height:37px;
|
height:37px;
|
||||||
padding:2px;
|
padding:2px;
|
||||||
z-index:101;
|
z-index:101;
|
||||||
}
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
.mentions-name:hover {
|
background-color:#e8eaed;
|
||||||
background-color:#e8eaed;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mentions-text {
|
.mentions-text {
|
||||||
@@ -46,6 +44,11 @@
|
|||||||
border-radius: 10%;
|
border-radius: 10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mention-fullname {
|
||||||
|
color: grey;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.mention-highlight {
|
.mention-highlight {
|
||||||
background-color:#fff2bb;
|
background-color:#fff2bb;
|
||||||
}
|
}
|
||||||
@@ -53,3 +56,8 @@
|
|||||||
.mention-link {
|
.mention-link {
|
||||||
color:$primary-color;
|
color:$primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mention-align {
|
||||||
|
position:relative;
|
||||||
|
top:5px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
.modal-body {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
.modal {
|
.modal {
|
||||||
&.image_modal {
|
&.image_modal {
|
||||||
.modal-backdrop.in {
|
.modal-backdrop.in {
|
||||||
@include opacity(0.7);
|
@include opacity(0.7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.info__label {
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
.remove__member {
|
.remove__member {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
@@ -13,7 +21,6 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
.modal-body {
|
.modal-body {
|
||||||
max-height: 75%;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.modal-push-down {
|
.modal-push-down {
|
||||||
@@ -29,7 +36,7 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: $primary-color;
|
background: $primary-color;
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
padding: 15px 15px 11px;
|
padding: 15px 15px 11px;
|
||||||
border: none;
|
border: none;
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
@include clearfix;
|
@include clearfix;
|
||||||
@@ -41,14 +48,23 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
button.close {
|
button.close {
|
||||||
margin-top: 0;
|
margin: -2px -2px 0 0;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@include opacity(1);
|
@include opacity(1);
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
@include single-transition(all, 0.25s, ease-in);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
line-height: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
margin-right: 10px;
|
|
||||||
&.btn-primary {
|
&.btn-primary {
|
||||||
float: right;
|
float: right;
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
@@ -102,7 +118,7 @@
|
|||||||
}
|
}
|
||||||
.more-channel-name {
|
.more-channel-name {
|
||||||
color: #444;
|
color: #444;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
@@ -127,17 +143,19 @@
|
|||||||
.modal-image {
|
.modal-image {
|
||||||
position:relative;
|
position:relative;
|
||||||
width:100%;
|
width:100%;
|
||||||
|
height: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
.image-wrapper {
|
.image-wrapper {
|
||||||
padding: 4px;
|
|
||||||
background: #FFF;
|
background: #FFF;
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
min-height: 280px;
|
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
@include border-radius(4px);
|
@include border-radius(3px);
|
||||||
display: table;
|
display: table;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
&:hover {
|
||||||
|
@include border-radius(3px 3px 0 0);
|
||||||
|
}
|
||||||
&:hover .modal-close {
|
&:hover .modal-close {
|
||||||
@include opacity(1);
|
@include opacity(1);
|
||||||
}
|
}
|
||||||
@@ -217,10 +235,11 @@
|
|||||||
}
|
}
|
||||||
.modal-button-bar {
|
.modal-button-bar {
|
||||||
position:absolute;
|
position:absolute;
|
||||||
bottom:0px;
|
bottom:-40px;
|
||||||
left:0px;
|
left:0px;
|
||||||
right:0px;
|
right:0px;
|
||||||
background-color:rgba(0, 0, 0, 0.8);
|
background-color: #222;
|
||||||
|
@include border-radius(0 0 3px 3px);
|
||||||
@include opacity(0);
|
@include opacity(0);
|
||||||
-webkit-transition: opacity 0.6s;
|
-webkit-transition: opacity 0.6s;
|
||||||
-moz-transition: opacity 0.6s;
|
-moz-transition: opacity 0.6s;
|
||||||
@@ -228,7 +247,6 @@
|
|||||||
transition: opacity 0.6s;
|
transition: opacity 0.6s;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
margin: 4px;
|
|
||||||
&.footer--show {
|
&.footer--show {
|
||||||
@include opacity(1);
|
@include opacity(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,10 +86,11 @@ body.ios {
|
|||||||
background: #FFF;
|
background: #FFF;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
font-weight: 900;
|
font-weight: 700;
|
||||||
@include border-radius(50px);
|
@include border-radius(50px);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.new-separator {
|
.new-separator {
|
||||||
@@ -212,6 +213,12 @@ body.ios {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.current--user {
|
||||||
|
.post-body {
|
||||||
|
@include border-radius(0 4px 4px 0);
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
&.same--root {
|
&.same--root {
|
||||||
.comment-icon__container {
|
.comment-icon__container {
|
||||||
@include opacity(0);
|
@include opacity(0);
|
||||||
@@ -249,6 +256,7 @@ body.ios {
|
|||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
margin: 0 0 5px;
|
margin: 0 0 5px;
|
||||||
|
font-size: 0.97em;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
.comment-icon__container {
|
.comment-icon__container {
|
||||||
@@ -269,7 +277,7 @@ body.ios {
|
|||||||
float: left;
|
float: left;
|
||||||
.post-profile-img {
|
.post-profile-img {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
@include border-radius(3px);
|
@include border-radius(50px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.post__content {
|
&.post__content {
|
||||||
@@ -290,6 +298,8 @@ body.ios {
|
|||||||
width: 600px;
|
width: 600px;
|
||||||
float: left;
|
float: left;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
padding: 0.3em 0.5em 0.1em;
|
||||||
|
margin: -0.3em 0 0;
|
||||||
.post-link {
|
.post-link {
|
||||||
@include clearfix;
|
@include clearfix;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -359,7 +369,7 @@ body.ios {
|
|||||||
.embed-title {
|
.embed-title {
|
||||||
margin: 3px 0 1px;
|
margin: 3px 0 1px;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
&.post--comment {
|
&.post--comment {
|
||||||
&.other--root {
|
&.other--root {
|
||||||
.post-comment {
|
.post-comment {
|
||||||
margin-left: 0;
|
margin-left: -7px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,11 +22,16 @@
|
|||||||
width: 825px;
|
width: 825px;
|
||||||
}
|
}
|
||||||
.post-body {
|
.post-body {
|
||||||
padding-left: 0;
|
width: 736px;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.post-body {
|
||||||
|
float: none;
|
||||||
|
width: 750px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
.post__content {
|
.post__content {
|
||||||
width: 920px;
|
width: 920px;
|
||||||
}
|
}
|
||||||
@@ -63,10 +68,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.post-body {
|
|
||||||
float: none;
|
|
||||||
width: 750px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
&.post--comment {
|
&.post--comment {
|
||||||
&.other--root {
|
&.other--root {
|
||||||
.post-comment {
|
.post-comment {
|
||||||
margin-left: 0;
|
margin-left: -7px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
margin-left: 60px;
|
margin-left: 60px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
border-left: 4px solid #EEE;
|
border-left: 4px solid #EEE;
|
||||||
|
margin-bottom: 10px;
|
||||||
div.post-profile-img__container {
|
div.post-profile-img__container {
|
||||||
.post-profile-img {
|
.post-profile-img {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -98,7 +100,7 @@
|
|||||||
width: 825px;
|
width: 825px;
|
||||||
}
|
}
|
||||||
.post-body {
|
.post-body {
|
||||||
padding-left: 0;
|
width: 736px;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -135,6 +137,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.post-body {
|
.post-body {
|
||||||
|
margin: 0;
|
||||||
float: none;
|
float: none;
|
||||||
width: 750px;
|
width: 750px;
|
||||||
}
|
}
|
||||||
@@ -231,17 +234,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modal {
|
.modal {
|
||||||
|
.info__label {
|
||||||
|
text-align: left;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
.modal-header {
|
.modal-header {
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
.modal-action {
|
.modal-action {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
button.close {
|
|
||||||
width: 35px;
|
|
||||||
height: 32px;
|
|
||||||
margin: -5px -10px 0;
|
|
||||||
}
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
float: none;
|
float: none;
|
||||||
}
|
}
|
||||||
@@ -261,9 +261,11 @@
|
|||||||
.settings-table {
|
.settings-table {
|
||||||
display: block;
|
display: block;
|
||||||
.settings-content {
|
.settings-content {
|
||||||
display: block;
|
&.minimize-settings {
|
||||||
.section-edit {
|
display: block;
|
||||||
text-align: left;
|
.section-edit {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.settings-links {
|
.settings-links {
|
||||||
@@ -287,10 +289,12 @@
|
|||||||
}
|
}
|
||||||
.settings-table {
|
.settings-table {
|
||||||
.settings-content {
|
.settings-content {
|
||||||
padding: 0;
|
&.minimize-settings {
|
||||||
display: none;
|
padding: 0;
|
||||||
.user-settings {
|
display: none;
|
||||||
padding: 70px 20px 30px;
|
.user-settings {
|
||||||
|
padding: 70px 20px 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.settings-links {
|
.settings-links {
|
||||||
@@ -538,7 +542,6 @@
|
|||||||
.modal {
|
.modal {
|
||||||
.modal-image {
|
.modal-image {
|
||||||
.image-wrapper {
|
.image-wrapper {
|
||||||
padding-bottom: 40px;
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
@include opacity(1);
|
@include opacity(1);
|
||||||
}
|
}
|
||||||
@@ -575,7 +578,7 @@
|
|||||||
&.post--comment {
|
&.post--comment {
|
||||||
&.other--root {
|
&.other--root {
|
||||||
.post-comment {
|
.post-comment {
|
||||||
margin-left: 11px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
.search-channel__name {
|
.search-channel__name {
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-edit {
|
.section-edit {
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
border-top:1px solid lightgrey;
|
border-top:1px solid lightgrey;
|
||||||
}
|
}
|
||||||
.post-profile-img {
|
.post-profile-img {
|
||||||
@include border-radius(3px);
|
@include border-radius(50px);
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
.member-name {
|
.member-name {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
color: #999;
|
color: #999;
|
||||||
&.unread-title {
|
&.unread-title {
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
background: #e6f2fa;
|
background: #e6f2fa;
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
color: #111;
|
color: #111;
|
||||||
background-color: #e1e1e1;
|
background-color: #e1e1e1;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
@import "partials/modal";
|
@import "partials/modal";
|
||||||
@import "partials/mentions";
|
@import "partials/mentions";
|
||||||
@import "partials/error";
|
@import "partials/error";
|
||||||
|
@import "partials/loading";
|
||||||
|
|
||||||
// Responsive Css
|
// Responsive Css
|
||||||
@import "partials/responsive";
|
@import "partials/responsive";
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
{{define "channel"}}
|
{{define "channel"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "find_team"}}
|
{{define "find_team"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body class="white">
|
<body class="white">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
|
<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 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">
|
<link rel="stylesheet" href="/static/css/styles.css">
|
||||||
|
|
||||||
<script src="/static/js/min/perfect-scrollbar.min.js"></script>
|
<script src="/static/js/min/perfect-scrollbar.min.js"></script>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "home"}}
|
{{define "home"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "login"}}
|
{{define "login"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body class="white">
|
<body class="white">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "password_reset"}}
|
{{define "password_reset"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body class="white">
|
<body class="white">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "signup_team"}}
|
{{define "signup_team"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body class="white">
|
<body class="white">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "signup_team_complete"}}
|
{{define "signup_team_complete"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body class="white">
|
<body class="white">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "signup_team_confirm"}}
|
{{define "signup_team_confirm"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body class="white">
|
<body class="white">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "signup_user_complete"}}
|
{{define "signup_user_complete"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body class="white">
|
<body class="white">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "verify"}}
|
{{define "verify"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{{define "welcome"}}
|
{{define "welcome"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
otherUserId = ids[0]
|
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)
|
api.Handle404(w, r)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user