Merge branch 'master' into mm-1391

Этот коммит содержится в:
=Corey Hulen
2015-07-05 09:02:23 -08:00
родитель 72bfc1ee2a 4a3003c0dc
Коммит 592d68cf41
77 изменённых файлов: 943 добавлений и 397 удалений

Просмотреть файл

@@ -10,13 +10,21 @@ Mattermost is a team communication service. It brings team messaging and file sh
We built Mattermost to help teams focus on what matters most to them. It works for us, we hope it works for you too.
Learn More
==========
<ul>
<li/>Ask the core team anything at: http://forum.mattermost.org</li>
<li/>Share feature requests and upvotes: http://www.mattermost.org/feature-requests/</li>
<li/>File bugs: http://www.mattermost.org/filing-issues/</li>
<li/>Make a pull request: http://www.mattermost.org/contribute-to-mattermost/</li>
</ul>
Installing the Mattermost
=========================
You're installing "Mattermost Preview", a pre-released 0.50 version intended for an early look at what we're building. While SpinPunch runs this version internally, it's not recommended for production deployments since we can't guarantee API stability or backwards compatibility until our 1.0 version release.
That said, any issues at all, please let us know on the Mattermost forum at: http://bit.ly/1MY1kul
That said, any issues at all, please let us know on the Mattermost forum at: http://forum.mattermost.org
Local Machine Setup (Docker)
-----------------------------
@@ -33,38 +41,38 @@ Local Machine Setup (Docker)
6. When docker is done fetching the image, open http://dockerhost:8065/ in your browser
### Ubuntu ###
1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summery below.
1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summary below.
`sudo apt-get update`
``` bash
sudo apt-get update
sudo apt-get install wget
wget -qO- https://get.docker.com/ | sh
sudo usermod -aG docker <username>
sudo service docker start
newgrp docker
```
`sudo apt-get install wget`
`wget -qO- https://get.docker.com/ | sh`
`sudo usermod -aG docker <username>`
`sudo service docker start`
`newgrp docker`
2. Run `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium
2. Run `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium`
3. When docker is done fetching the image, open http://localhost:8065/ in your browser
### Arch ###
1. Install docker using the following commands
1. Install docker using the following commands:
`pacman -S docker`
``` bash
pacman -S docker
systemctl enable docker.service
systemctl start docker.service
gpasswd -a <username> docker
newgrp docker
```
`systemctl enable docker.service`
2. Start docker container:
`systemctl start docker.service`
``` bash
docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium
```
`gpasswd -a <username> docker`
`newgrp docker`
2. docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium
3. When docker is done fetching the image, open http://localhost:8065/ in your browser
3. When docker is done fetching the image, open http://localhost:8065/ in your browser.
### Notes ###
If your ISP blocks port 25 then you may install locally but email will not be sent.
@@ -80,6 +88,8 @@ If you wish to remove mattermost-dev use the following commands
1. `docker stop mattermost-dev`
2. `docker rm -v mattermost-dev`
If you wish to gain access to the container use the following commands
1. `docker exec -ti mattermost-dev /bin/bash`
AWS Elastic Beanstalk Setup (Docker)
------------------------------------
@@ -89,7 +99,7 @@ AWS Elastic Beanstalk Setup (Docker)
2. Select "Create New Application" from the top right.
3. Name the application and press next
4. Select "Create a web server" environment.
5. If asked, select create and AIM role and instance profile and press next.
5. If asked, select create and IAM role and instance profile and press next.
6. For predefined configuration select docker. For environment type select single instance.
7. For application source, select upload your own and upload Dockerrun.aws.json from docker/Dockerrun.aws.json. Everything else may be left at default.
8. Select an environment name, this is how you will refer to your environment. Make sure the URL is available then press next.
@@ -107,7 +117,7 @@ AWS Elastic Beanstalk Setup (Docker)
18. Modify an existing CNAME record set or create a new one with the name * and the value of the domain you copied in step 1.13.
19. Save the record set
3. Set the enviroment variable "MATTERMOST\_DOMAIN" to the domain you mapped above (example.com not www.example.com)
3. Set the environment variable "MATTERMOST\_DOMAIN" to the domain you mapped above (example.com not www.example.com)
20. Return the Elastic Beanstalk from the AWS console.
21. Select the environment you created.
22. Select configuration from the sidebar.
@@ -119,6 +129,11 @@ AWS Elastic Beanstalk Setup (Docker)
26. Return to the dashboard on the sidebar and wait for beanstalk update the environment.
27. Try it out by entering the domain you mapped into your browser.
Contributing
------------
To contribute to this open source project please review the Mattermost Contribution Guidelines at http://www.mattermost.org/contribute-to-mattermost/.
License
-------
@@ -126,3 +141,4 @@ Most Mattermost source files are made available under the terms of the GNU Affer
As an exception, Admin Tools and Configuration Files are are made available under the terms of the Apache License, version 2.0. See LICENSE.txt for more information.

Просмотреть файл

@@ -57,7 +57,7 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if sc, err := CreateChannel(c, channel, r.URL.Path, true); err != nil {
if sc, err := CreateChannel(c, channel, true); err != nil {
c.Err = err
return
} else {
@@ -65,7 +65,7 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func CreateChannel(c *Context, channel *model.Channel, path string, addMember bool) (*model.Channel, *model.AppError) {
func CreateChannel(c *Context, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
if result := <-Srv.Store.Channel().Save(channel); result.Err != nil {
return nil, result.Err
} else {
@@ -100,7 +100,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if sc, err := CreateDirectChannel(c, userId, r.URL.Path); err != nil {
if sc, err := CreateDirectChannel(c, userId); err != nil {
c.Err = err
return
} else {
@@ -108,7 +108,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Channel, *model.AppError) {
func CreateDirectChannel(c *Context, otherUserId string) (*model.Channel, *model.AppError) {
if len(otherUserId) != 26 {
return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId)
}
@@ -132,7 +132,7 @@ func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Ch
return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId)
}
if sc, err := CreateChannel(c, channel, path, true); err != nil {
if sc, err := CreateChannel(c, channel, true); err != nil {
return nil, err
} else {
cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId,
@@ -146,6 +146,23 @@ func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Ch
}
}
func CreateDefaultChannels(c *Context, teamId string) ([]*model.Channel, *model.AppError) {
townSquare := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: teamId}
if _, err := CreateChannel(c, townSquare, false); err != nil {
return nil, err
}
offTopic := &model.Channel{DisplayName: "Off-Topic", Name: "off-topic", Type: model.CHANNEL_OPEN, TeamId: teamId}
if _, err := CreateChannel(c, offTopic, false); err != nil {
return nil, err
}
channels := []*model.Channel{townSquare, offTopic}
return channels, nil
}
func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
channel := model.ChannelFromJson(r.Body)
@@ -303,7 +320,7 @@ func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
channelId := params["id"]
JoinChannel(c, channelId, r.URL.Path)
JoinChannel(c, channelId, "")
if c.Err != nil {
return
@@ -314,7 +331,7 @@ func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(result)))
}
func JoinChannel(c *Context, channelId string, path string) {
func JoinChannel(c *Context, channelId string, role string) {
sc := Srv.Store.Channel().Get(channelId)
uc := Srv.Store.User().Get(c.Session.UserId)
@@ -340,7 +357,7 @@ func JoinChannel(c *Context, channelId string, path string) {
}
if channel.Type == model.CHANNEL_OPEN {
cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId, NotifyLevel: model.CHANNEL_NOTIFY_ALL}
cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: role}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
c.Err = cmresult.Err
@@ -363,6 +380,32 @@ func JoinChannel(c *Context, channelId string, path string) {
}
}
func JoinDefaultChannels(c *Context, user *model.User, channelRole string) *model.AppError {
// We don't call JoinChannel here since c.Session is not populated on user creation
var err *model.AppError = nil
if result := <-Srv.Store.Channel().GetByName(user.TeamId, "town-square"); result.Err != nil {
err = result.Err
} else {
cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
err = cmResult.Err
}
}
if result := <-Srv.Store.Channel().GetByName(user.TeamId, "off-topic"); result.Err != nil {
err = result.Err
} else {
cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
err = cmResult.Err
}
}
return err
}
func leaveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)

Просмотреть файл

@@ -35,8 +35,15 @@ func TestCreateChannel(t *testing.T) {
}
rget := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
if rget.Channels[0].Name != channel.Name {
t.Fatal("full name didn't match")
nameMatch := false
for _, c := range rget.Channels {
if c.Name == channel.Name {
nameMatch = true
}
}
if !nameMatch {
t.Fatal("Did not create channel with correct name")
}
if _, err := Client.CreateChannel(rchannel.Data.(*model.Channel)); err == nil {
@@ -679,6 +686,8 @@ func TestUpdateNotifyLevel(t *testing.T) {
data["user_id"] = user.Id
data["notify_level"] = model.CHANNEL_NOTIFY_MENTION
timeBeforeUpdate := model.GetMillis()
if _, err := Client.UpdateNotifyLevel(data); err != nil {
t.Fatal(err)
}
@@ -689,6 +698,10 @@ func TestUpdateNotifyLevel(t *testing.T) {
t.Fatal("NotifyLevel did not update properly")
}
if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
t.Fatal("LastUpdateAt did not update")
}
data["user_id"] = "junk"
if _, err := Client.UpdateNotifyLevel(data); err == nil {
t.Fatal("Should have errored - bad user id")
@@ -735,7 +748,7 @@ func TestUpdateNotifyLevel(t *testing.T) {
}
func TestFuzzyChannel(t *testing.T) {
Setup();
Setup()
team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -747,9 +760,9 @@ func TestFuzzyChannel(t *testing.T) {
Client.LoginByEmail(team.Domain, user.Email, "pwd")
// Strings that should pass as acceptable channel names
var fuzzyStringsPass = []string {
var fuzzyStringsPass = []string{
"*", "?", ".", "}{][)(><", "{}[]()<>",
"qahwah ( قهوة)",
"שָׁלוֹם עֲלֵיכֶם",
"Ramen チャーシュー chāshū",

Просмотреть файл

@@ -197,7 +197,7 @@ func joinCommand(c *Context, command *model.Command) bool {
return false
}
JoinChannel(c, v.Id, "/command")
JoinChannel(c, v.Id, "")
if c.Err != nil {
return false

Просмотреть файл

@@ -129,7 +129,7 @@ func TestJoinCommands(t *testing.T) {
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
if len(c1.Channels) != 3 { // 3 because of town-square and direct
if len(c1.Channels) != 4 { // 4 because of town-square, off-topic and direct
t.Fatal("didn't join channel")
}

Просмотреть файл

@@ -227,7 +227,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
channel = result.Data.(*model.Channel)
if channel.Type == model.CHANNEL_DIRECT {
bodyText = "You have one new message."
subjectText = "New Direct Message"
subjectText = "New Private Message"
} else {
bodyText = "You have one new mention."
subjectText = "New Mention"
@@ -273,7 +273,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
} else {
// Find out who is a member of the channel only keep those profiles
// Find out who is a member of the channel, only keep those profiles
if eResult := <-echan; eResult.Err != nil {
l4g.Error("Failed to get channel members channel_id=%v err=%v", post.ChannelId, eResult.Err.Message)
return
@@ -306,13 +306,23 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
}
}
}
// Add @all to keywords if user has them turned on
if profile.NotifyProps["all"] == "true" {
keywordMap["@all"] = append(keywordMap["@all"], profile.Id)
}
// Add @channel to keywords if user has them turned on
if profile.NotifyProps["channel"] == "true" {
keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id)
}
}
// Build a map as a list of unique user_ids that are mentioned in this post
splitF := func(c rune) bool {
return model.SplitRunes[c]
}
splitMessage := strings.FieldsFunc(strings.Replace(post.Message, "<br>", " ", -1), splitF)
splitMessage := strings.FieldsFunc(post.Message, splitF)
for _, word := range splitMessage {
// Non-case-sensitive check for regular keys

Просмотреть файл

@@ -146,10 +146,8 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
rteam := result.Data.(*model.Team)
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id}
if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil {
c.Err = err
if _, err := CreateDefaultChannels(c, rteam.Id); err != nil {
c.Err = nil
return
}
@@ -197,10 +195,8 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
rteam := result.Data.(*model.Team)
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id}
if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil {
c.Err = err
if _, err := CreateDefaultChannels(c, rteam.Id); err != nil {
c.Err = nil
return
}
@@ -488,12 +484,21 @@ func InviteMembers(team *model.Team, user *model.User, invites []string) {
} else {
sender = user.FullName
}
senderRole := ""
if strings.Contains(user.Roles, model.ROLE_ADMIN) || strings.Contains(user.Roles, model.ROLE_SYSTEM_ADMIN) {
senderRole = "administrator"
} else {
senderRole = "member"
}
subjectPage := NewServerTemplatePage("invite_subject", teamUrl)
subjectPage.Props["SenderName"] = sender
subjectPage.Props["TeamName"] = team.Name
bodyPage := NewServerTemplatePage("invite_body", teamUrl)
bodyPage.Props["TeamName"] = team.Name
bodyPage.Props["SenderName"] = sender
bodyPage.Props["SenderStatus"] = senderRole
bodyPage.Props["Email"] = invite

Просмотреть файл

@@ -55,6 +55,11 @@ func TestCreateFromSignupTeam(t *testing.T) {
}
}
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
if len(c1.Channels) != 2 {
t.Fatal("default channels not created")
}
ts.Data = "garbage"
_, err = Client.CreateTeamFromSignup(&ts)
if err == nil {
@@ -71,6 +76,17 @@ func TestCreateTeam(t *testing.T) {
t.Fatal(err)
}
user := &model.User{TeamId: rteam.Data.(*model.Team).Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
Srv.Store.User().VerifyEmail(user.Id)
Client.LoginByEmail(team.Domain, user.Email, "pwd")
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
if len(c1.Channels) != 2 {
t.Fatal("default channels not created")
}
if rteam.Data.(*model.Team).Name != team.Name {
t.Fatal("full name didn't match")
}

Просмотреть файл

@@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
<link href='https://fonts.googleapis.com/css?family=Lato:400,700,900' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,600,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body class="white error">

Просмотреть файл

@@ -18,7 +18,7 @@
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2>
<p>{{.Props.TeamName}} started using {{.SiteName}}.<br> The team administrator <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamName}}</strong>.</p>
<p>{{.Props.TeamName}} started using {{.SiteName}}.<br> The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamName}}</strong>.</p>
<p style="margin: 20px 0 15px">
<a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Join Team</a>
</p>

Просмотреть файл

@@ -20,7 +20,7 @@
<h2 style="font-weight: normal; margin-top: 10px;">You were mentioned</h2>
<p>CHANNEL: {{.Props.ChannelName}}<br>{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} GMT, {{.Props.Month}} {{.Props.Day}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif;">{{.Props.PostMessage}}</pre></p>
<p style="margin: 20px 0 15px">
<a href="{{.Props.TeamLink}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a>
<a href="{{.Props.TeamLink}}" style="background: #2389D7; display: inline-block; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 170px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a>
</p>
</td>
</tr>

Просмотреть файл

@@ -176,21 +176,16 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
} else {
ruser := result.Data.(*model.User)
// Do not error if user cannot be added to the town-square channel
if cresult := <-Srv.Store.Channel().GetByName(team.Id, "town-square"); cresult.Err != nil {
l4g.Error("Failed to get town-square err=%v", cresult.Err)
} else {
cm := &model.ChannelMember{ChannelId: cresult.Data.(*model.Channel).Id, UserId: ruser.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
l4g.Error("Failed to add member town-square err=%v", cmresult.Err)
}
// Soft error if there is an issue joining the default channels
if err := JoinDefaultChannels(c, ruser, channelRole); err != nil {
l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err)
}
//fireAndForgetWelcomeEmail(strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl+"/channels/town-square")
if user.EmailVerified {
if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
l4g.Error("Failed to get town-square err=%v", cresult.Err)
l4g.Error("Failed to set email verified err=%v", cresult.Err)
}
} else {
FireAndForgetVerifyEmail(result.Data.(*model.User).Id, strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl)
@@ -198,7 +193,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
ruser.Sanitize(map[string]bool{})
//This message goes to every channel, so the channelId is irrelevant
// This message goes to every channel, so the channelId is irrelevant
message := model.NewMessage(team.Id, "", ruser.Id, model.ACTION_NEW_USER)
store.PublishAndForget(message)

Просмотреть файл

@@ -119,7 +119,7 @@ func TestSocket(t *testing.T) {
}
func TestZZWebScoketTearDown(t *testing.T) {
func TestZZWebSocketTearDown(t *testing.T) {
// *IMPORTANT* - Kind of hacky
// This should be the last function in any test file
// that calls Setup()

Просмотреть файл

@@ -10,7 +10,7 @@
"ServiceSettings": {
"SiteName": "Mattermost",
"Domain": "",
"Mode" : "prod",
"Mode" : "dev",
"AllowTesting" : false,
"UseSSL": false,
"Port": "80",

Просмотреть файл

@@ -78,7 +78,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
createdTeam := result.Data.(*model.Team)
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: createdTeam.Id}
if _, err := api.CreateChannel(c, channel, r.URL.Path, false); err != nil {
if _, err := api.CreateChannel(c, channel, false); err != nil {
c.Err = err
return
}

Просмотреть файл

@@ -53,6 +53,12 @@ func (o *ChannelList) Etag() string {
t = member.LastViewedAt
id = v.Id
}
if member.LastUpdateAt > t {
t = member.LastUpdateAt
id = v.Id
}
}
}

Просмотреть файл

@@ -25,6 +25,7 @@ type ChannelMember struct {
MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"`
NotifyLevel string `json:"notify_level"`
LastUpdateAt int64 `json:"last_update_at"`
}
func (o *ChannelMember) ToJson() string {
@@ -70,6 +71,10 @@ func (o *ChannelMember) IsValid() *AppError {
return nil
}
func (o *ChannelMember) PreSave() {
o.LastUpdateAt = GetMillis()
}
func IsChannelNotifyLevelValid(notifyLevel string) bool {
return notifyLevel == CHANNEL_NOTIFY_ALL || notifyLevel == CHANNEL_NOTIFY_MENTION || notifyLevel == CHANNEL_NOTIFY_NONE || notifyLevel == CHANNEL_NOTIFY_QUIET
}

Просмотреть файл

@@ -16,7 +16,7 @@ const (
ROLE_SYSTEM_ADMIN = "system_admin"
ROLE_SYSTEM_SUPPORT = "system_support"
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
USER_OFFLINE = "offline"
USER_AWAY = "away"
USER_ONLINE = "online"
@@ -147,10 +147,13 @@ func (u *User) SetDefaultNotifications() {
u.NotifyProps["email"] = "true"
u.NotifyProps["desktop"] = USER_NOTIFY_ALL
u.NotifyProps["desktop_sound"] = "true"
u.NotifyProps["mention_keys"] = u.Username
u.NotifyProps["first_name"] = "true"
u.NotifyProps["mention_keys"] = u.Username + ",@" + u.Username
u.NotifyProps["first_name"] = "false"
u.NotifyProps["all"] = "true"
u.NotifyProps["channel"] = "true"
splitName := strings.Split(u.FullName, " ")
if len(splitName) > 0 && splitName[0] != "" {
u.NotifyProps["first_name"] = "true"
u.NotifyProps["mention_keys"] += "," + splitName[0]
}
}
@@ -277,17 +280,17 @@ func ComparePassword(hash string, password string) bool {
func IsUsernameValid(username string) bool {
var restrictedUsernames = []string {
var restrictedUsernames = []string{
BOT_USERNAME,
"all",
"channel",
}
for _,restrictedUsername := range restrictedUsernames {
for _, restrictedUsername := range restrictedUsernames {
if username == restrictedUsername {
return false
}
}
}
return true
}

Просмотреть файл

@@ -17,7 +17,7 @@ import (
)
const (
ETAG_ROOT_VERSION = "10"
ETAG_ROOT_VERSION = "11"
)
type StringMap map[string]string
@@ -260,7 +260,7 @@ func Etag(parts ...interface{}) string {
return etag
}
var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_]*[A-Za-z0-9])$`)
var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$`)
var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`)
var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`)

Просмотреть файл

@@ -37,6 +37,7 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
}
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
s.CreateColumnIfNotExists("ChannelMembers", "LastUpdateAt", "NotifyLevel", "bigint(20)", "0") // Remove after 6/7/2015 prod push
}
func (s SqlChannelStore) CreateIndexesIfNotExists() {
@@ -273,6 +274,7 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
go func() {
result := StoreResult{}
member.PreSave()
if result.Err = member.IsValid(); result.Err != nil {
storeChannel <- result
return
@@ -484,7 +486,8 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelId string, userId string) Sto
SET
ChannelMembers.MentionCount = 0,
ChannelMembers.MsgCount = Channels.TotalMsgCount,
ChannelMembers.LastViewedAt = Channels.LastPostAt
ChannelMembers.LastViewedAt = Channels.LastPostAt,
ChannelMembers.LastUpdateAt = Channels.LastPostAt
WHERE
Channels.Id = ChannelMembers.ChannelId
AND UserId = ?
@@ -533,15 +536,18 @@ func (s SqlChannelStore) UpdateNotifyLevel(channelId, userId, notifyLevel string
go func() {
result := StoreResult{}
updateAt := model.GetMillis()
_, err := s.GetMaster().Exec(
`UPDATE
ChannelMembers
SET
NotifyLevel = ?
NotifyLevel = ?,
LastUpdateAt = ?
WHERE
UserId = ?
AND ChannelId = ?`,
notifyLevel, userId, channelId)
notifyLevel, updateAt, userId, channelId)
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.UpdateNotifyLevel", "We couldn't update the notify level", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
}

Просмотреть файл

@@ -356,9 +356,14 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
go func() {
result := StoreResult{}
termMap := map[string]bool{}
searchType := "Message"
if isHashtagSearch {
searchType = "Hashtags"
for _,term := range strings.Split(terms, " ") {
termMap[term] = true;
}
}
// @ has a speical meaning in INNODB FULLTEXT indexes and
@@ -394,6 +399,17 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
list := &model.PostList{Order: make([]string, 0, len(posts))}
for _, p := range posts {
if searchType == "Hashtags" {
exactMatch := false
for _, tag := range strings.Split(p.Hashtags, " ") {
if termMap[tag] {
exactMatch = true
}
}
if !exactMatch {
continue
}
}
list.AddPost(p)
list.AddOrder(p.Id)
}

Просмотреть файл

@@ -7,6 +7,9 @@ import (
l4g "code.google.com/p/log4go"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
crand "crypto/rand"
dbsql "database/sql"
"encoding/base64"
@@ -327,20 +330,26 @@ func encrypt(key []byte, text string) (string, error) {
}
plaintext := []byte(text)
skey := sha512.Sum512(key)
ekey, akey := skey[:32], skey[32:]
block, err := aes.NewCipher(key)
block, err := aes.NewCipher(ekey)
if err != nil {
return "", err
}
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
macfn := hmac.New(sha256.New, akey)
ciphertext := make([]byte, aes.BlockSize+macfn.Size()+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(crand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
stream.XORKeyStream(ciphertext[aes.BlockSize+macfn.Size():], plaintext)
macfn.Write(ciphertext[aes.BlockSize+macfn.Size():])
mac := macfn.Sum(nil)
copy(ciphertext[aes.BlockSize:aes.BlockSize+macfn.Size()], mac)
return base64.URLEncoding.EncodeToString(ciphertext), nil
}
@@ -351,9 +360,26 @@ func decrypt(key []byte, cryptoText string) (string, error) {
return "{}", nil
}
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
ciphertext, err := base64.URLEncoding.DecodeString(cryptoText)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
skey := sha512.Sum512(key)
ekey, akey := skey[:32], skey[32:]
macfn := hmac.New(sha256.New, akey)
if len(ciphertext) < aes.BlockSize+macfn.Size() {
return "", errors.New("short ciphertext")
}
macfn.Write(ciphertext[aes.BlockSize+macfn.Size():])
expectedMac := macfn.Sum(nil)
mac := ciphertext[aes.BlockSize:aes.BlockSize+macfn.Size()]
if hmac.Equal(expectedMac, mac) != true {
return "", errors.New("Incorrect MAC for the given ciphertext")
}
block, err := aes.NewCipher(ekey)
if err != nil {
return "", err
}
@@ -362,7 +388,7 @@ func decrypt(key []byte, cryptoText string) (string, error) {
return "", errors.New("ciphertext too short")
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
ciphertext = ciphertext[aes.BlockSize+macfn.Size():]
stream := cipher.NewCFBDecrypter(block, iv)

Просмотреть файл

@@ -11,6 +11,7 @@ import (
"net"
"net/mail"
"net/smtp"
"html"
)
func CheckMailSettings() *model.AppError {
@@ -84,7 +85,7 @@ func SendMail(to, subject, body string) *model.AppError {
headers := make(map[string]string)
headers["From"] = fromMail.String()
headers["To"] = toMail.String()
headers["Subject"] = subject
headers["Subject"] = html.UnescapeString(subject)
headers["MIME-version"] = "1.0"
headers["Content-Type"] = "text/html"

Просмотреть файл

@@ -15,17 +15,8 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
function getExtraInfoStateFromStores() {
return {
extra_info: ChannelStore.getCurrentExtraInfo()
};
}
var ExtraMembers = React.createClass({
componentDidMount: function() {
ChannelStore.addExtraInfoChangeListener(this._onChange);
ChannelStore.addChangeListener(this._onChange);
var originalLeave = $.fn.popover.Constructor.prototype.leave;
$.fn.popover.Constructor.prototype.leave = function(obj) {
var self = obj instanceof this.constructor ? obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type);
@@ -49,27 +40,21 @@ var ExtraMembers = React.createClass({
});
},
componentWillUnmount: function() {
ChannelStore.removeExtraInfoChangeListener(this._onChange);
ChannelStore.removeChangeListener(this._onChange);
},
_onChange: function() {
var newState = getExtraInfoStateFromStores();
if (!utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
},
getInitialState: function() {
return getExtraInfoStateFromStores();
},
render: function() {
var count = this.state.extra_info.members.length == 0 ? "-" : this.state.extra_info.members.length;
count = this.state.extra_info.members.length > 19 ? "20+" : count;
var count = this.props.members.length == 0 ? "-" : this.props.members.length;
count = this.props.members.length > 19 ? "20+" : count;
var data_content = "";
var sortedMembers = this.props.members;
this.state.extra_info.members.forEach(function(m) {
data_content += "<div style='white-space: nowrap'>" + m.username + "</div>";
});
if(sortedMembers) {
sortedMembers.sort(function(a,b) {
return a.username.localeCompare(b.username);
})
sortedMembers.forEach(function(m) {
data_content += "<div style='white-space: nowrap'>" + m.username + "</div>";
});
}
return (
<div style={{"cursor" : "pointer"}} id="member_popover" data-toggle="popover" data-content={data_content} data-original-title="Members" >
@@ -228,7 +213,7 @@ module.exports = React.createClass({
<a href="#"><strong className="heading">{channelTitle}</strong></a>
}
</th>
<th><ExtraMembers channelId={this.state.channel.id} /></th>
<th><ExtraMembers members={this.state.users} channelId={this.state.channel.id} /></th>
{ searchForm }
<th>
<div className="dropdown" style={{"marginLeft":"5px", "marginRight":"10px"}}>

Просмотреть файл

@@ -35,9 +35,18 @@ module.exports = React.createClass({
<h4 className="modal-title" id="myModalLabel">{channel.display_name}</h4>
</div>
<div className="modal-body">
<p><strong>Channel Name: </strong>{channel.display_name}</p>
<p><strong>Channel Handle: </strong>{channel.name}</p>
<p><strong>Channel ID: </strong>{channel.id}</p>
<div className="row form-group">
<div className="col-sm-3 info__label">Channel Name: </div>
<div className="col-sm-9">{channel.display_name}</div>
</div>
<div className="row form-group">
<div className="col-sm-3 info__label">Channel Handle:</div>
<div className="col-sm-9">{channel.name}</div>
</div>
<div className="row">
<div className="col-sm-3 info__label">Channel ID:</div>
<div className="col-sm-9">{channel.id}</div>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>

Просмотреть файл

@@ -1,6 +1,8 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx');
var utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
@@ -9,26 +11,50 @@ var ChannelStore = require('../stores/channel_store.jsx');
module.exports = React.createClass({
componentDidMount: function() {
ChannelStore.addChangeListener(this._onChange);
var self = this;
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
var button = e.relatedTarget;
var channel_id = button.dataset.channelid;
var notifyLevel = ChannelStore.getMember(channel_id).notify_level;
self.setState({ notify_level: notifyLevel, title: button.dataset.title, channel_id: channel_id });
var quietMode = false;
if (notifyLevel === "quiet") quietMode = true;
self.setState({ notify_level: notifyLevel, quiet_mode: quietMode, title: button.dataset.title, channel_id: channel_id });
});
},
getInitialState: function() {
return { notify_level: "", title: "", channel_id: "" };
componentWillUnmount: function() {
ChannelStore.removeChangeListener(this._onChange);
},
handleUpdate: function(e) {
_onChange: function() {
if (!this.state.channel_id) return;
var notifyLevel = ChannelStore.getMember(this.state.channel_id).notify_level;
var quietMode = false;
if (notifyLevel === "quiet") quietMode = true;
var newState = this.state;
newState.notify_level = notifyLevel;
newState.quiet_mode = quietMode;
if (!utils.areStatesEqual(this.state, newState)) {
this.setState(newState);
}
},
updateSection: function(section) {
this.setState({ activeSection: section });
},
getInitialState: function() {
return { notify_level: "", title: "", channel_id: "", activeSection: "" };
},
handleUpdate: function() {
var channel_id = this.state.channel_id;
var notify_level = this.state.notify_level;
var notify_level = this.state.quiet_mode ? "quiet" : this.state.notify_level;
var data = {};
data["channel_id"] = channel_id;
data["user_id"] = UserStore.getCurrentId();
data["notify_level"] = this.state.notify_level;
data["notify_level"] = notify_level;
if (!data["notify_level"] || data["notify_level"].length === 0) return;
@@ -37,7 +63,7 @@ module.exports = React.createClass({
var member = ChannelStore.getMember(channel_id);
member.notify_level = notify_level;
ChannelStore.setChannelMember(member);
$(this.refs.modal.getDOMNode()).modal('hide');
this.updateSection("");
}.bind(this),
function(err) {
this.setState({ server_error: err.message });
@@ -45,42 +71,138 @@ module.exports = React.createClass({
);
},
handleRadioClick: function(notifyLevel) {
this.setState({ notify_level: notifyLevel });
this.setState({ notify_level: notifyLevel, quiet_mode: false });
this.refs.modal.getDOMNode().focus();
},
handleQuietToggle: function() {
if (this.state.notify_level === "quiet") {
this.setState({ notify_level: "none" });
this.refs.modal.getDOMNode().focus();
} else {
this.setState({ notify_level: "quiet" });
this.refs.modal.getDOMNode().focus();
}
handleQuietToggle: function(quietMode) {
this.setState({ notify_level: "none", quiet_mode: quietMode });
this.refs.modal.getDOMNode().focus();
},
render: function() {
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
var allActive = "";
var mentionActive = "";
var noneActive = "";
var quietActive = "";
var desktopHidden = "";
var self = this;
if (this.state.notify_level === "quiet") {
desktopHidden = "hidden";
quietActive = "active";
} else if (this.state.notify_level === "mention") {
mentionActive = "active";
} else if (this.state.notify_level === "none") {
noneActive = "active";
var desktopSection;
if (this.state.activeSection === 'desktop') {
var notifyActive = [false, false, false];
if (this.state.notify_level === "mention") {
notifyActive[1] = true;
} else if (this.state.notify_level === "all") {
notifyActive[0] = true;
} else {
notifyActive[2] = true;
}
var inputs = [];
inputs.push(
<div>
<div className="radio">
<label>
<input type="radio" checked={notifyActive[0]} onClick={function(){self.handleRadioClick("all")}}>For all activity</input>
</label>
<br/>
</div>
<div className="radio">
<label>
<input type="radio" checked={notifyActive[1]} onClick={function(){self.handleRadioClick("mention")}}>Only for mentions</input>
</label>
<br/>
</div>
<div className="radio">
<label>
<input type="radio" checked={notifyActive[2]} onClick={function(){self.handleRadioClick("none")}}>Never</input>
</label>
</div>
</div>
);
desktopSection = (
<SettingItemMax
title="Send desktop notifications"
inputs={inputs}
submit={this.handleUpdate}
server_error={server_error}
updateSection={function(e){self.updateSection("");self._onChange();e.preventDefault();}}
/>
);
} else {
allActive = "active";
var describe = "";
if (this.state.notify_level === "mention") {
describe = "Only for mentions";
} else if (this.state.notify_level === "all") {
describe = "For all activity";
} else {
describe = "Never";
}
desktopSection = (
<SettingItemMin
title="Send desktop notifications"
describe={describe}
updateSection={function(e){self.updateSection("desktop");e.preventDefault();}}
/>
);
}
var quietSection;
if (this.state.activeSection === 'quiet') {
var quietActive = ["",""];
if (this.state.quiet_mode) {
quietActive[0] = "active";
} else {
quietActive[1] = "active";
}
var inputs = [];
inputs.push(
<div>
<div className="btn-group" data-toggle="buttons-radio">
<button className={"btn btn-default "+quietActive[0]} onClick={function(){self.handleQuietToggle(true)}}>On</button>
<button className={"btn btn-default "+quietActive[1]} onClick={function(){self.handleQuietToggle(false)}}>Off</button>
</div>
</div>
);
inputs.push(
<div>
<br/>
Enabling quiet mode will turn off desktop notifications and only mark the channel as unread if you have been mentioned.
</div>
);
quietSection = (
<SettingItemMax
title="Quiet mode"
inputs={inputs}
submit={this.handleUpdate}
server_error={server_error}
updateSection={function(e){self.updateSection("");self._onChange();e.preventDefault();}}
/>
);
} else {
var describe = "";
if (this.state.quiet_mode) {
describe = "On";
} else {
describe = "Off";
}
quietSection = (
<SettingItemMin
title="Quiet mode"
describe={describe}
updateSection={function(e){self.updateSection("quiet");e.preventDefault();}}
/>
);
}
var self = this;
return (
<div className="modal fade" id="channel_notifications" ref="modal" tabIndex="-1" role="dialog" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-dialog settings-modal">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal">
@@ -90,31 +212,23 @@ module.exports = React.createClass({
<h4 className="modal-title">{"Notification Preferences for " + this.state.title}</h4>
</div>
<div className="modal-body">
<div className={desktopHidden}>
<span>Desktop Notifications</span>
<br/>
<div className="btn-group" data-toggle="buttons-radio">
<button className={"btn btn-default "+allActive} onClick={function(){self.handleRadioClick("all")}}>Any activity (default)</button>
<button className={"btn btn-default "+mentionActive} onClick={function(){self.handleRadioClick("mention")}}>Mentions of my name</button>
<button className={"btn btn-default "+noneActive} onClick={function(){self.handleRadioClick("none")}}>Nothing</button>
<div className="settings-table">
<div className="settings-content">
<div ref="wrapper" className="user-settings">
<br/>
<div className="divider-dark first"/>
{desktopSection}
<div className="divider-light"/>
{quietSection}
<div className="divider-dark"/>
</div>
<br/>
<br/>
</div>
<span>Quiet Mode</span>
<br/>
<div className="btn-group" data-toggle="buttons-checkbox">
<button className={"btn btn-default "+quietActive} onClick={this.handleQuietToggle}>Quiet Mode</button>
</div>
{ server_error }
</div>
<div className="modal-footer">
<button type="button" className="btn btn-primary" onClick={this.handleUpdate}>Done</button>
</div>
</div>
</div>
</div>
);
}
});

Просмотреть файл

@@ -112,13 +112,28 @@ module.exports = React.createClass({
return { messageText: '', uploadsInProgress: 0, previews: [], submitting: false };
},
setUploads: function(val) {
var num = this.state.uploadsInProgress + val;
this.setState({uploadsInProgress: num});
var oldInProgress = this.state.uploadsInProgress
var newInProgress = oldInProgress + val;
if (newInProgress + this.state.previews.length > Constants.MAX_UPLOAD_FILES) {
newInProgress = Constants.MAX_UPLOAD_FILES - this.state.previews.length;
this.setState({limit_error: "Uploads limited to " + Constants.MAX_UPLOAD_FILES + " files maximum. Please use additional comments for more files."});
} else {
this.setState({limit_error: null});
}
var numToUpload = newInProgress - oldInProgress;
if (numToUpload <= 0) return 0;
this.setState({uploadsInProgress: newInProgress});
return numToUpload;
},
render: function() {
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
var post_error = this.state.post_error ? <label className='control-label'>{this.state.post_error}</label> : null;
var limit_error = this.state.limit_error ? <div className='has-error'><label className='control-label'>{this.state.limit_error}</label></div> : null;
var preview = <div/>;
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
@@ -129,13 +144,6 @@ module.exports = React.createClass({
uploadsInProgress={this.state.uploadsInProgress} />
);
}
var limit_previews = ""
if (this.state.previews.length > 5) {
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: While all files will be available, only first five will show thumbnails." }</label></div>
}
if (this.state.previews.length > 20) {
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: Uploads limited to 20 files maximum. Please use additional posts for more files." }</label></div>
}
return (
<form onSubmit={this.handleSubmit}>
@@ -159,7 +167,7 @@ module.exports = React.createClass({
<input type="button" className="btn btn-primary comment-btn pull-right" value="Add Comment" onClick={this.handleSubmit} />
{ post_error }
{ server_error }
{ limit_previews }
{ limit_error }
</div>
</div>
{ preview }

Просмотреть файл

@@ -51,7 +51,7 @@ module.exports = React.createClass({
false,
function(data) {
PostStore.storeDraft(data.channel_id, user_id, null);
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null });
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null, limit_error: null });
if (data.goto_location.length > 0) {
window.location.href = data.goto_location;
@@ -71,7 +71,7 @@ module.exports = React.createClass({
client.createPost(post, ChannelStore.getCurrent(),
function(data) {
PostStore.storeDraft(data.channel_id, data.user_id, null);
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null });
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null, limit_error: null });
this.resizePostHolder();
AsyncClient.getPosts(true);
@@ -207,21 +207,36 @@ module.exports = React.createClass({
return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText };
},
setUploads: function(val) {
var num = this.state.uploadsInProgress + val;
var oldInProgress = this.state.uploadsInProgress
var newInProgress = oldInProgress + val;
if (newInProgress + this.state.previews.length > Constants.MAX_UPLOAD_FILES) {
newInProgress = Constants.MAX_UPLOAD_FILES - this.state.previews.length;
this.setState({limit_error: "Uploads limited to " + Constants.MAX_UPLOAD_FILES + " files maximum. Please use additional posts for more files."});
} else {
this.setState({limit_error: null});
}
var numToUpload = newInProgress - oldInProgress;
if (numToUpload <= 0) return 0;
var draft = PostStore.getCurrentDraft();
if (!draft) {
draft = {}
draft['message'] = '';
draft['previews'] = [];
}
draft['uploadsInProgress'] = num;
draft['uploadsInProgress'] = newInProgress;
PostStore.storeCurrentDraft(draft);
this.setState({uploadsInProgress: num});
this.setState({uploadsInProgress: newInProgress});
return numToUpload;
},
render: function() {
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
var post_error = this.state.post_error ? <label className='control-label'>{this.state.post_error}</label> : null;
var limit_error = this.state.limit_error ? <div className='has-error'><label className='control-label'>{this.state.limit_error}</label></div> : null;
var preview = <div/>;
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
@@ -232,13 +247,6 @@ module.exports = React.createClass({
uploadsInProgress={this.state.uploadsInProgress} />
);
}
var limit_previews = ""
if (this.state.previews.length > 5) {
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: While all files will be available, only first five will show thumbnails." }</label></div>
}
if (this.state.previews.length > 20) {
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: Uploads limited to 20 files maximum. Please use additional posts for more files." }</label></div>
}
return (
<form id="create_post" ref="topDiv" role="form" onSubmit={this.handleSubmit}>
@@ -260,7 +268,7 @@ module.exports = React.createClass({
<div className={post_error ? 'post-create-footer has-error' : 'post-create-footer'}>
{ post_error }
{ server_error }
{ limit_previews }
{ limit_error }
{ preview }
<MsgTyping channelId={this.state.channel_id} parentId=""/>
</div>

Просмотреть файл

@@ -10,7 +10,7 @@ var Constants = require('../utils/constants.jsx');
module.exports = React.createClass({
handleRemove: function(e) {
var previewDiv = e.target.parentNode.parentNode;
this.props.onRemove(previewDiv.dataset.filename);
this.props.onRemove(previewDiv.getAttribute('data-filename'));
},
render: function() {
var previews = [];

Просмотреть файл

@@ -12,18 +12,18 @@ module.exports = React.createClass({
this.props.onUploadError(null);
//This looks redundant, but must be done this way due to
//setState being an asynchronous call
// This looks redundant, but must be done this way due to
// setState being an asynchronous call
var numFiles = 0;
for(var i = 0; i < files.length && i <= 20 ; i++) {
for(var i = 0; i < files.length && i < Constants.MAX_UPLOAD_FILES; i++) {
if (files[i].size <= Constants.MAX_FILE_SIZE) {
numFiles++;
}
}
this.props.setUploads(numFiles);
var numToUpload = this.props.setUploads(numFiles);
for (var i = 0; i < files.length && i <= 20; i++) {
for (var i = 0; i < files.length && i < numToUpload; i++) {
if (files[i].size > Constants.MAX_FILE_SIZE) {
this.props.onUploadError("Files must be no more than " + Constants.MAX_FILE_SIZE/1000000 + " MB");
continue;
@@ -70,8 +70,8 @@ module.exports = React.createClass({
self.props.onUploadError(null);
//This looks redundant, but must be done this way due to
//setState being an asynchronous call
// This looks redundant, but must be done this way due to
// setState being an asynchronous call
var items = e.clipboardData.items;
var numItems = 0;
if (items) {
@@ -87,9 +87,9 @@ module.exports = React.createClass({
}
}
self.props.setUploads(numItems);
var numToUpload = self.props.setUploads(numItems);
for (var i = 0; i < items.length; i++) {
for (var i = 0; i < items.length && i < numToUpload; i++) {
if (items[i].type.indexOf("image") !== -1) {
var file = items[i].getAsFile();

Просмотреть файл

@@ -42,7 +42,7 @@ module.exports = React.createClass({
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
<button data-copy-btn type="button" className="btn btn-primary" data-clipboard-text={this.state.value}>Copy Link</button>
<button data-copy-btn type="button" className="btn btn-primary pull-left" data-clipboard-text={this.state.value}>Copy Link</button>
</div>
</div>
</div>

Просмотреть файл

@@ -37,7 +37,7 @@ var FindTeamDomain = React.createClass({
window.location.href = window.location.protocol + "//" + domain + "." + utils.getDomainWithOutSub();
}
else {
this.state.server_error = "We couldn't find your " + strings.TeamPlural + ".";
this.state.server_error = "We couldn't find your " + strings.Team + ".";
this.setState(this.state);
}
}.bind(this),

Просмотреть файл

@@ -6,10 +6,16 @@ module.exports = React.createClass({
this.props.handleClick(this.props.username);
},
render: function() {
var icon;
if (this.props.id != null) {
icon = <span><img className="mention-img" src={"/api/v1/users/" + this.props.id + "/image"}/></span>;
} else {
icon = <span><i className="mention-img fa fa-users fa-2x"></i></span>;
}
return (
<div className="mentions-name" onClick={this.handleClick}>
<img className="pull-left mention-img" src={"/api/v1/users/" + this.props.id + "/image"}/>
<span>@{this.props.username}</span><span style={{'color':'grey', 'marginLeft':'10px'}}>{this.props.name}</span>
<div className="pull-left">{icon}</div>
<div className="pull-left mention-align"><span>@{this.props.username}</span><span className="mention-fullname">{this.props.secondary_text}</span></div>
</div>
);
}

Просмотреть файл

@@ -23,6 +23,11 @@ module.exports = React.createClass({
}
}
);
$(document).click(function() {
if($('#'+self.props.id).length && $('#'+self.props.id).get(0) !== $(':focus').get(0)) {
self.setState({mentionText: "-1"})
}
});
},
componentWillUnmount: function() {
PostStore.removeMentionDataChangeListener(this._onChange);
@@ -74,6 +79,18 @@ module.exports = React.createClass({
users.push(profiles[id]);
}
var all = {};
all.username = "all";
all.full_name = "";
all.secondary_text = "Notifies everyone in the team";
users.push(all);
var channel = {};
channel.username = "channel";
channel.full_name = "";
channel.secondary_text = "Notifies everyone in the channel";
users.push(channel);
users.sort(function(a,b) {
if (a.username < b.username) return -1;
if (a.username > b.username) return 1;
@@ -91,6 +108,7 @@ module.exports = React.createClass({
var splitName = users[i].full_name.split(' ');
firstName = splitName[0].toLowerCase();
lastName = splitName.length > 1 ? splitName[splitName.length-1].toLowerCase() : "";
users[i].secondary_text = users[i].full_name;
}
if (firstName.lastIndexOf(mentionText,0) === 0
@@ -99,7 +117,7 @@ module.exports = React.createClass({
<Mention
ref={'mention' + index}
username={users[i].username}
name={users[i].full_name}
secondary_text={users[i].secondary_text}
id={users[i].id}
handleClick={this.handleClick} />
);

Просмотреть файл

@@ -49,7 +49,7 @@ module.exports = React.createClass({
<span aria-hidden="true">&times;</span>
<span className="sr-only">Close</span>
</button>
<h4 className="modal-title">More Direct Messages</h4>
<h4 className="modal-title">More Private Messages</h4>
</div>
<div className="modal-body">
<ul className="nav nav-pills nav-stacked">

Просмотреть файл

@@ -6,13 +6,14 @@ var PostBody = require('./post_body.jsx');
var PostInfo = require('./post_info.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Constants = require('../utils/constants.jsx');
var UserStore = require('../stores/user_store.jsx');
var ActionTypes = Constants.ActionTypes;
module.exports = React.createClass({
componentDidMount: function() {
$('.edit-modal').on('show.bs.modal', function () {
$('.edit-modal .edit-modal-body').css('overflow-y', 'auto');
$('.edit-modal .edit-modal-body').css('max-height', $(window).height() * 0.7);
$('.modal').on('show.bs.modal', function () {
$('.modal-body').css('overflow-y', 'auto');
$('.modal-body').css('max-height', $(window).height() * 0.7);
});
},
handleCommentClick: function(e) {
@@ -56,7 +57,7 @@ module.exports = React.createClass({
var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null;
if(this.props.sameRoot){
if (this.props.sameRoot){
rootUser = "same--root";
}
else {
@@ -64,13 +65,18 @@ module.exports = React.createClass({
}
var postType = "";
if(type != "Post"){
if (type != "Post"){
postType = "post--comment";
}
var currentUserCss = "";
if (UserStore.getCurrentId() === post.user_id) {
currentUserCss = "current--user";
}
return (
<div>
<div id={post.id} className={"post " + this.props.sameUser + " " + rootUser + " " + postType}>
<div id={post.id} className={"post " + this.props.sameUser + " " + rootUser + " " + postType + " " + currentUserCss}>
{ !this.props.hideProfilePic ?
<div className="post-profile-img__container">
<img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image"} height="36" width="36" />

Просмотреть файл

@@ -102,7 +102,7 @@ module.exports = React.createClass({
images.push(filenames[i]);
} else if (i < Constants.MAX_DISPLAY_FILES) {
postFiles.push(
<div className="post-image__column custom-file" key={fileInfo.name}>
<div className="post-image__column custom-file" key={fileInfo.name+i}>
<a href={fileInfo.path+"."+fileInfo.ext} download={fileInfo.name+"."+fileInfo.ext}>
<div className={"file-icon "+utils.getIconClassName(type)}/>
</a>

Просмотреть файл

@@ -295,7 +295,7 @@ module.exports = React.createClass({
},
render: function() {
var order = [];
var posts = {};
var posts;
var last_viewed = Number.MAX_VALUE;
@@ -324,13 +324,7 @@ module.exports = React.createClass({
if (order.length > 0 && order.length % Constants.POST_CHUNK_SIZE === 0) {
more_messages = <a ref="loadmore" className="more-messages-text theme" href="#" onClick={this.getMorePosts}>Load more messages</a>;
} else if (channel.type === 'D') {
var userIds = channel.name.split('__');
var teammate;
if (userIds.length === 2 && userIds[0] === user_id) {
teammate = UserStore.getProfile(userIds[1]);
} else if (userIds.length === 2 && userIds[1] === user_id) {
teammate = UserStore.getProfile(userIds[0]);
}
var teammate = utils.getDirectTeammate(channel.id)
if (teammate) {
var teammate_name = teammate.full_name.length > 0 ? teammate.full_name : teammate.username;
@@ -342,13 +336,13 @@ module.exports = React.createClass({
<div className="channel-intro-profile">
<strong><UserProfile userId={teammate.id} /></strong>
</div>
<p className="channel-intro-text">{"This is the start of your direct message history with " + teammate_name + "." }<br/>{"Direct messages and files shared here are not shown to people outside this area."}</p>
<p className="channel-intro-text">{"This is the start of your private message history with " + teammate_name + "." }<br/>{"Private messages and files shared here are not shown to people outside this area."}</p>
</div>
);
} else {
more_messages = (
<div className="channel-intro">
<p className="channel-intro-text">{"This is the start of your direct message history with this " + strings.Team + "mate. Direct messages and files shared here are not shown to people outside this area."}</p>
<p className="channel-intro-text">{"This is the start of your private message history with this " + strings.Team + "mate. Private messages and files shared here are not shown to people outside this area."}</p>
</div>
);
}
@@ -356,6 +350,7 @@ module.exports = React.createClass({
var ui_name = channel.display_name
var members = ChannelStore.getCurrentExtraInfo().members;
var creator_name = "";
var userStyle = { color: UserStore.getCurrentUser().props.theme }
for (var i = 0; i < members.length; i++) {
if (members[i].roles.indexOf('admin') > -1) {
@@ -382,8 +377,18 @@ module.exports = React.createClass({
</p>
</div>
);
} else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
more_messages = (
<div className="channel-intro">
<h4 className="channel-intro-title">Welcome</h4>
<p>
{"This is the start of " + ui_name + ", a channel for conversations youd prefer out of more focused channels."}
<br/>
<a className="intro-links" href="#" style={userStyle} data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={ui_name} data-channelid={channel.id}><i className="fa fa-pencil"></i>Set a description</a>
</p>
</div>
);
} else {
var userStyle = { color: UserStore.getCurrentUser().props.theme }
var ui_type = channel.type === 'P' ? "private group" : "channel";
more_messages = (
<div className="channel-intro">
@@ -403,55 +408,70 @@ module.exports = React.createClass({
}
var postCtls = [];
var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date();
var currentPostDay = new Date();
for (var i = order.length-1; i >= 0; i--) {
var post = posts[order[i]];
var parentPost;
if (posts != undefined) {
var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date();
var currentPostDay = new Date();
if (post.parent_id) {
parentPost = posts[post.parent_id];
} else {
parentPost = null;
for (var i = order.length-1; i >= 0; i--) {
var post = posts[order[i]];
var parentPost;
if (post.parent_id) {
parentPost = posts[post.parent_id];
} else {
parentPost = null;
}
var sameUser = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && post.create_at - posts[order[i+1]].create_at <= 1000*60*5 ? "same--user" : "";
var sameRoot = i < order.length-1 && post.root_id != "" && (posts[order[i+1]].id === post.root_id || posts[order[i+1]].root_id === post.root_id) ? true : false;
// we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
var hideProfilePic = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && posts[order[i+1]].root_id === '' && post.root_id === '';
// check if it's the last comment in a consecutive string of comments on the same post
var isLastComment = false;
if (utils.isComment(post)) {
// it is the last comment if it is last post in the channel or the next post has a different root post
isLastComment = (i === 0 || posts[order[i-1]].root_id != post.root_id);
}
var postCtl = <Post sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} />;
currentPostDay = utils.getDateForUnixTicks(post.create_at);
if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) {
postCtls.push(
<div className="date-separator">
<hr className="separator__hr" />
<div className="separator__text">{currentPostDay.toDateString()}</div>
</div>
);
}
if (post.create_at > last_viewed && !rendered_last_viewed) {
rendered_last_viewed = true;
postCtls.push(
<div className="new-separator">
<hr id="new_message" className="separator__hr" />
<div className="separator__text">New Messages</div>
</div>
);
}
postCtls.push(postCtl);
previousPostDay = utils.getDateForUnixTicks(post.create_at);
}
var sameUser = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && post.create_at - posts[order[i+1]].create_at <= 1000*60*5 ? "same--user" : "";
var sameRoot = i < order.length-1 && post.root_id != "" && (posts[order[i+1]].id === post.root_id || posts[order[i+1]].root_id === post.root_id) ? true : false;
// we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
var hideProfilePic = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && posts[order[i+1]].root_id === '' && post.root_id === '';
// check if it's the last comment in a consecutive string of comments on the same post
var isLastComment = false;
if (utils.isComment(post)) {
// it is the last comment if it is last post in the channel or the next post has a different root post
isLastComment = (i === 0 || posts[order[i-1]].root_id != post.root_id);
}
var postCtl = <Post sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} />;
currentPostDay = utils.getDateForUnixTicks(post.create_at);
if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) {
postCtls.push(
<div className="date-separator">
<hr className="separator__hr" />
<div className="separator__text">{currentPostDay.toDateString()}</div>
}
else {
postCtls.push(
<div ref="loadingscreen" className="loading-screen">
<div className="loading__content">
<h3>Loading</h3>
<div id="round_1" className="round"></div>
<div id="round_2" className="round"></div>
<div id="round_3" className="round"></div>
</div>
);
}
if (post.create_at > last_viewed && !rendered_last_viewed) {
rendered_last_viewed = true;
postCtls.push(
<div className="new-separator">
<hr id="new_message" className="separator__hr" />
<div className="separator__text">New Messages</div>
</div>
);
}
postCtls.push(postCtl);
previousPostDay = utils.getDateForUnixTicks(post.create_at);
</div>
);
}
return (

Просмотреть файл

@@ -68,9 +68,14 @@ RootPost = React.createClass({
var filenames = this.props.post.filenames;
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
var type = "Post"
var type = "Post";
if (this.props.post.root_id.length > 0) {
type = "Comment"
type = "Comment";
}
var currentUserCss = "";
if (UserStore.getCurrentId() === this.props.post.user_id) {
currentUserCss = "current--user";
}
if (filenames) {
@@ -84,7 +89,7 @@ RootPost = React.createClass({
if (fileSplit.length < 2) continue;
var ext = fileSplit[fileSplit.length-1];
fileSplit.splice(fileSplit.length-1,1)
fileSplit.splice(fileSplit.length-1,1);
var filePath = fileSplit.join('.');
var filename = filePath.split('/')[filePath.split('/').length-1];
@@ -111,7 +116,7 @@ RootPost = React.createClass({
}
return (
<div className="post post--root">
<div className={"post post--root " + currentUserCss}>
<div className="post-profile-img__container">
<img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" />
</div>
@@ -170,6 +175,11 @@ CommentPost = React.createClass({
var commentClass = "post";
var currentUserCss = "";
if (UserStore.getCurrentId() === this.props.post.user_id) {
currentUserCss = "current--user";
}
var postImageModalId = "rhs_comment_view_image_modal_" + this.props.post.id;
var filenames = this.props.post.filenames;
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
@@ -219,7 +229,7 @@ CommentPost = React.createClass({
var message = utils.textToJsx(this.props.post.message);
return (
<div className={commentClass}>
<div className={commentClass + " " + currentUserCss}>
<div className="post-profile-img__container">
<img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" />
</div>

Просмотреть файл

@@ -43,6 +43,7 @@ SearchItem = React.createClass({
e.preventDefault();
var self = this;
client.getPost(
this.props.post.channel_id,
this.props.post.id,
@@ -64,6 +65,11 @@ SearchItem = React.createClass({
dispatchError(err, "getPost");
}
);
var postChannel = ChannelStore.get(this.props.post.channel_id);
var teammate = postChannel.type === 'D' ? utils.getDirectTeammate(this.props.post.channel_id).username : "";
utils.switchChannel(postChannel,teammate);
},
render: function() {
@@ -73,7 +79,7 @@ SearchItem = React.createClass({
if (channel) {
if (channel.type === 'D') {
channelName = "Direct Message";
channelName = "Private Message";
} else {
channelName = channel.display_name;
}

Просмотреть файл

@@ -13,7 +13,7 @@ module.exports = React.createClass({
<li className="col-sm-12 section-title">{this.props.title}</li>
<li className="col-sm-9 col-sm-offset-3">
<ul className="setting-list">
<li className="row setting-list-item form-group">
<li className="setting-list-item">
{inputs}
</li>
<li className="setting-list-item">

Просмотреть файл

@@ -269,13 +269,8 @@ var SidebarLoggedIn = React.createClass({
var channel = ChannelStore.getCurrent();
if (channel) {
if (channel.type === 'D') {
userIds = channel.name.split('__');
if (userIds.length < 2) return;
if (userIds[0] == UserStore.getCurrentId() && UserStore.getProfile(userIds[1])) {
document.title = UserStore.getProfile(userIds[1]).username + " " + document.title.substring(document.title.lastIndexOf("-"));
} else if (userIds[1] == UserStore.getCurrentId() && UserStore.getProfile(userIds[0])) {
document.title = UserStore.getProfile(userIds[0]).username + " " + document.title.substring(document.title.lastIndexOf("-"));
}
var teammate_username = utils.getDirectTeammate(channel.id).username
document.title = teammate_username + " " + document.title.substring(document.title.lastIndexOf("-"));
} else {
document.title = channel.display_name + " " + document.title.substring(document.title.lastIndexOf("-"))
}
@@ -414,7 +409,7 @@ var SidebarLoggedIn = React.createClass({
{privateChannelItems}
</ul>
<ul className="nav nav-pills nav-stacked">
<li><h4>Direct Messages</h4></li>
<li><h4>Private Messages</h4></li>
{directMessageItems}
{ this.state.hideDirectChannels.length > 0 ?
<li><a href="#" data-toggle="modal" className="nav-more" data-target="#more_direct_channels" data-channels={JSON.stringify(this.state.hideDirectChannels)}>{"More ("+this.state.hideDirectChannels.length+")"}</a></li>

Просмотреть файл

@@ -60,6 +60,7 @@ module.exports = React.createClass({
<div className="nav-pills__container">
<ul className="nav nav-pills nav-stacked">
<li><a href="#" data-toggle="modal" data-target="#user_settings1"><i className="glyphicon glyphicon-cog"></i>Account Settings</a></li>
{ isAdmin ? <li><a href="#" data-toggle="modal" data-target="#team_settings"><i className="glyphicon glyphicon-globe"></i>Team Settings</a></li> : "" }
{ invite_link }
{ team_link }
{ manage_link }

Просмотреть файл

@@ -164,7 +164,9 @@ TeamUrlPage = React.createClass({
}
var cleaned_name = utils.cleanUpUrlable(name);
if (cleaned_name != name) {
var urlRegex = /^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g;
if (cleaned_name != name || !urlRegex.test(name)) {
this.setState({name_error: "Must be lowercase alphanumeric characters"});
return;
}
@@ -337,7 +339,7 @@ EmailItem = React.createClass({
return false;
}
else if (email === teamEmail) {
this.state.email_error = "Please use an a different email than the one used at signup";
this.state.email_error = "Please use a different email than the one used at signup";
this.setState(this.state);
return false;
}

Просмотреть файл

@@ -78,13 +78,14 @@ var FeatureTab = React.createClass({
<button className={"btn btn-default "+valetActive[0]} onClick={function(){self.handleValetRadio("true")}}>On</button>
<button className={"btn btn-default "+valetActive[1]} onClick={function(){self.handleValetRadio("false")}}>Off</button>
</div>
<div><br/>Warning: Turning on the Valet feature and using it with any third party software increases the risk of a security breach.</div>
<div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div>
<br></br>
</div>
);
valetSection = (
<SettingItemMax
title="Valet"
title="Valet (Preview - EXPERTS ONLY)"
inputs={inputs}
submit={this.submitValetFeature}
server_error={server_error}
@@ -102,7 +103,7 @@ var FeatureTab = React.createClass({
valetSection = (
<SettingItemMin
title="Valet"
title="Valet (Preview - EXPERTS ONLY)"
describe={describe}
updateSection={function(){self.props.updateSection("valet");}}
/>
@@ -113,7 +114,7 @@ var FeatureTab = React.createClass({
<div>
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 className="modal-title" ref="title"><i className="modal-back"></i>General Settings</h4>
<h4 className="modal-title" ref="title"><i className="modal-back"></i>Feature Settings</h4>
</div>
<div ref="wrapper" className="user-settings">
<h3 className="tab-header">Feature Settings</h3>

Просмотреть файл

@@ -45,7 +45,7 @@ module.exports = React.createClass({
updateTab={this.updateTab}
/>
</div>
<div className="settings-content">
<div className="settings-content minimize-settings">
<TeamSettings
activeTab={this.state.active_tab}
activeSection={this.state.active_section}

Просмотреть файл

@@ -153,7 +153,7 @@ module.exports = React.createClass({
var mentions = [];
for (var i = 0; i < matches.length; i++) {
var m = matches[i].substring(1,matches[i].length).trim();
if (m in profileMap && mentions.indexOf(m) === -1) {
if ((m in profileMap && mentions.indexOf(m) === -1) || Constants.SPECIAL_MENTIONS.indexOf(m) !== -1) {
mentions.push(m);
}
}

Просмотреть файл

@@ -10,8 +10,7 @@ function getStateFromStores(userId) {
if (profile == null) {
return { profile: { id: "0", username: "..."} };
}
else {
} else {
return { profile: profile };
}
}
@@ -54,12 +53,11 @@ module.exports = React.createClass({
var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username;
var data_content = ""
data_content += "<img style='margin: 10px' src='/api/v1/users/" + this.state.profile.id + "/image' height='128' width='128' />"
var data_content = "<img style='margin: 10px' src='/api/v1/users/" + this.state.profile.id + "/image' height='128' width='128' />";
if (!config.ShowEmail) {
data_content += "<div><span style='white-space:nowrap;'>Email not shared</span></div>";
data_content += "<div class='text-nowrap'>Email not shared</div>";
} else {
data_content += "<div><a href='mailto:'" + this.state.profile.email + "'' style='white-space:nowrap;text-transform:lowercase;'>" + this.state.profile.email + "</a></div>";
data_content += "<div><a href='mailto:" + this.state.profile.email + "' class='text-nowrap text-lowercase'>" + this.state.profile.email + "</a></div>";
}
return (

Просмотреть файл

@@ -20,11 +20,10 @@ function getNotificationsStateFromStores() {
var mention_key = false;
var custom_keys = "";
var first_name_key = false;
var all_key = false;
var channel_key = false;
if (!user.notify_props) {
mention_keys = user.username;
if (user.full_name.length > 0) mention_keys += ","+ user.full_name.split(" ")[0];
} else {
if (user.notify_props) {
if (user.notify_props.mention_keys !== undefined) {
var keys = user.notify_props.mention_keys.split(',');
@@ -48,9 +47,17 @@ function getNotificationsStateFromStores() {
if (user.notify_props.first_name !== undefined) {
first_name_key = user.notify_props.first_name === "true";
}
if (user.notify_props.all !== undefined) {
all_key = user.notify_props.all === "true";
}
if (user.notify_props.channel !== undefined) {
channel_key = user.notify_props.channel === "true";
}
}
return { notify_level: desktop, enable_email: email, enable_sound: sound, username_key: username_key, mention_key: mention_key, custom_keys: custom_keys, custom_keys_checked: custom_keys.length > 0, first_name_key: first_name_key };
return { notify_level: desktop, enable_email: email, enable_sound: sound, username_key: username_key, mention_key: mention_key, custom_keys: custom_keys, custom_keys_checked: custom_keys.length > 0, first_name_key: first_name_key, all_key: all_key, channel_key: channel_key };
}
@@ -73,6 +80,8 @@ var NotificationsTab = React.createClass({
data["mention_keys"] = string_keys;
data["first_name"] = this.state.first_name_key ? "true" : "false";
data["all"] = this.state.all_key ? "true" : "false";
data["channel"] = this.state.channel_key ? "true" : "false";
client.updateUserNotifyProps(data,
function(data) {
@@ -120,6 +129,12 @@ var NotificationsTab = React.createClass({
updateFirstNameKey: function(val) {
this.setState({ first_name_key: val });
},
updateAllKey: function(val) {
this.setState({ all_key: val });
},
updateChannelKey: function(val) {
this.setState({ channel_key: val });
},
updateCustomMentionKeys: function() {
var checked = this.refs.customcheck.getDOMNode().checked;
@@ -155,7 +170,7 @@ var NotificationsTab = React.createClass({
var inputs = [];
inputs.push(
<div className="col-sm-12">
<div>
<div className="radio">
<label>
<input type="radio" checked={notifyActive[0]} onClick={function(){self.handleNotifyRadio("all")}}>For all activity</input>
@@ -164,7 +179,7 @@ var NotificationsTab = React.createClass({
</div>
<div className="radio">
<label>
<input type="radio" checked={notifyActive[1]} onClick={function(){self.handleNotifyRadio("mention")}}>Only for mentions and direct messages</input>
<input type="radio" checked={notifyActive[1]} onClick={function(){self.handleNotifyRadio("mention")}}>Only for mentions and private messages</input>
</label>
<br/>
</div>
@@ -188,7 +203,7 @@ var NotificationsTab = React.createClass({
} else {
var describe = "";
if (this.state.notify_level === "mention") {
describe = "Only for mentions and direct messages";
describe = "Only for mentions and private messages";
} else if (this.state.notify_level === "none") {
describe = "Never";
} else {
@@ -216,7 +231,7 @@ var NotificationsTab = React.createClass({
var inputs = [];
inputs.push(
<div className="col-sm-12">
<div>
<div className="btn-group" data-toggle="buttons-radio">
<button className={"btn btn-default "+soundActive[0]} onClick={function(){self.handleSoundRadio("true")}}>On</button>
<button className={"btn btn-default "+soundActive[1]} onClick={function(){self.handleSoundRadio("false")}}>Off</button>
@@ -262,12 +277,12 @@ var NotificationsTab = React.createClass({
var inputs = [];
inputs.push(
<div className="col-sm-12">
<div>
<div className="btn-group" data-toggle="buttons-radio">
<button className={"btn btn-default "+emailActive[0]} onClick={function(){self.handleEmailRadio("true")}}>On</button>
<button className={"btn btn-default "+emailActive[1]} onClick={function(){self.handleEmailRadio("false")}}>Off</button>
</div>
<div><br/>{"Email notifications are sent for mentions and direct messages after you have been away from " + config.SiteName + " for 5 minutes."}</div>
<div><br/>{"Email notifications are sent for mentions and private messages after you have been away from " + config.SiteName + " for 5 minutes."}</div>
</div>
);
@@ -309,7 +324,7 @@ var NotificationsTab = React.createClass({
if (first_name != "") {
inputs.push(
<div className="col-sm-12">
<div>
<div className="checkbox">
<label>
<input type="checkbox" checked={this.state.first_name_key} onChange={function(e){self.updateFirstNameKey(e.target.checked);}}>{'Your case sensitive first name "' + first_name + '"'}</input>
@@ -320,7 +335,7 @@ var NotificationsTab = React.createClass({
}
inputs.push(
<div className="col-sm-12">
<div>
<div className="checkbox">
<label>
<input type="checkbox" checked={this.state.username_key} onChange={function(e){self.updateUsernameKey(e.target.checked);}}>{'Your non-case sensitive username "' + user.username + '"'}</input>
@@ -330,7 +345,7 @@ var NotificationsTab = React.createClass({
);
inputs.push(
<div className="col-sm-12">
<div>
<div className="checkbox">
<label>
<input type="checkbox" checked={this.state.mention_key} onChange={function(e){self.updateMentionKey(e.target.checked);}}>{'Your username mentioned "@' + user.username + '"'}</input>
@@ -340,7 +355,27 @@ var NotificationsTab = React.createClass({
);
inputs.push(
<div className="col-sm-12">
<div>
<div className="checkbox">
<label>
<input type="checkbox" checked={this.state.all_key} onChange={function(e){self.updateAllKey(e.target.checked);}}>{'Team-wide mentions "@all"'}</input>
</label>
</div>
</div>
);
inputs.push(
<div>
<div className="checkbox">
<label>
<input type="checkbox" checked={this.state.channel_key} onChange={function(e){self.updateChannelKey(e.target.checked);}}>{'Channel-wide mentions "@channel"'}</input>
</label>
</div>
</div>
);
inputs.push(
<div>
<div className="checkbox">
<label>
<input ref="customcheck" type="checkbox" checked={this.state.custom_keys_checked} onChange={this.updateCustomMentionKeys}>{'Other non-case sensitive words, separated by commas:'}</input>
@@ -369,6 +404,8 @@ var NotificationsTab = React.createClass({
}
if (this.state.username_key) keys.push(this.props.user.username);
if (this.state.mention_key) keys.push('@'+this.props.user.username);
if (this.state.all_key) keys.push('@all');
if (this.state.channel_key) keys.push('@channel');
if (this.state.custom_keys.length > 0) keys = keys.concat(this.state.custom_keys.split(','));
var describe = "";
@@ -622,7 +659,7 @@ var SecurityTab = React.createClass({
var inputs = [];
inputs.push(
<div>
<div className="form-group">
<label className="col-sm-5 control-label">Current Password</label>
<div className="col-sm-7">
<input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/>
@@ -630,7 +667,7 @@ var SecurityTab = React.createClass({
</div>
);
inputs.push(
<div>
<div className="form-group">
<label className="col-sm-5 control-label">New Password</label>
<div className="col-sm-7">
<input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/>
@@ -638,7 +675,7 @@ var SecurityTab = React.createClass({
</div>
);
inputs.push(
<div>
<div className="form-group">
<label className="col-sm-5 control-label">Retype New Password</label>
<div className="col-sm-7">
<input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/>
@@ -772,6 +809,11 @@ var GeneralTab = React.createClass({
if(!this.submitActive) return;
if(this.state.picture.type !== "image/jpeg") {
this.setState({client_error: "Only JPG images may be used for profile pictures"});
return;
}
formData = new FormData();
formData.append('image', this.state.picture, this.state.picture.name);
@@ -802,11 +844,13 @@ var GeneralTab = React.createClass({
updatePicture: function(e) {
if (e.target.files && e.target.files[0]) {
this.setState({ picture: e.target.files[0] });
this.submitActive = true;
this.setState({client_error:null})
} else {
this.setState({ picture: null });
}
this.submitActive = true
},
updateSection: function(section) {
this.setState({client_error:""})
@@ -837,7 +881,7 @@ var GeneralTab = React.createClass({
var inputs = [];
inputs.push(
<div>
<div className="form-group">
<label className="col-sm-5 control-label">First Name</label>
<div className="col-sm-7">
<input className="form-control" type="text" onChange={this.updateFirstName} value={this.state.first_name}/>
@@ -846,7 +890,7 @@ var GeneralTab = React.createClass({
);
inputs.push(
<div>
<div className="form-group">
<label className="col-sm-5 control-label">Last Name</label>
<div className="col-sm-7">
<input className="form-control" type="text" onChange={this.updateLastName} value={this.state.last_name}/>
@@ -879,7 +923,7 @@ var GeneralTab = React.createClass({
var inputs = [];
inputs.push(
<div>
<div className="form-group">
<label className="col-sm-5 control-label">{utils.isMobile() ? "": "Username"}</label>
<div className="col-sm-7">
<input className="form-control" type="text" onChange={this.updateUsername} value={this.state.username}/>
@@ -911,7 +955,7 @@ var GeneralTab = React.createClass({
var inputs = [];
inputs.push(
<div>
<div className="form-group">
<label className="col-sm-5 control-label">Primary Email</label>
<div className="col-sm-7">
<input className="form-control" type="text" onChange={this.updateEmail} value={this.state.email}/>
@@ -947,7 +991,7 @@ var GeneralTab = React.createClass({
submit={this.submitPicture}
src={"/api/v1/users/" + user.id + "/image"}
server_error={server_error}
client_error={email_error}
client_error={client_error}
updateSection={function(e){self.updateSection("");e.preventDefault();}}
picture={this.state.picture}
pictureChange={this.updatePicture}
@@ -1048,7 +1092,7 @@ var AppearanceTab = React.createClass({
var inputs = [];
inputs.push(
<li className="row setting-list-item form-group">
<li className="setting-list-item">
<div className="btn-group" data-toggle="buttons-radio">
{ theme_buttons }
</div>

Просмотреть файл

@@ -50,7 +50,7 @@ module.exports = React.createClass({
updateTab={this.updateTab}
/>
</div>
<div className="settings-content">
<div className="settings-content minimize-settings">
<UserSettings
activeTab={this.state.active_tab}
activeSection={this.state.active_section}

Просмотреть файл

@@ -240,6 +240,9 @@ var UserStore = assign({}, EventEmitter.prototype, {
if (first.length > 0) keys.push(first);
}
if (user.notify_props.all === "true") keys.push('@all');
if (user.notify_props.channel === "true") keys.push('@channel');
return keys;
} else {
return [];

Просмотреть файл

@@ -36,6 +36,7 @@ module.exports = {
SERVER_ACTION: null,
VIEW_ACTION: null
}),
SPECIAL_MENTIONS: ['all', 'channel'],
CHARACTER_LIMIT: 4000,
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png'],
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac'],
@@ -48,8 +49,10 @@ module.exports = {
PATCH_TYPES: ['patch'],
ICON_FROM_TYPE: {'audio': 'audio', 'video': 'video', 'spreadsheet': 'ppt', 'pdf': 'pdf', 'code': 'code' , 'word': 'word' , 'excel': 'excel' , 'patch': 'patch', 'other': 'generic'},
MAX_DISPLAY_FILES: 5,
MAX_UPLOAD_FILES: 5,
MAX_FILE_SIZE: 50000000, // 50 MB
DEFAULT_CHANNEL: 'town-square',
OFFTOPIC_CHANNEL: 'off-topic',
POST_CHUNK_SIZE: 60,
RESERVED_DOMAINS: [
"www",

Просмотреть файл

@@ -2,6 +2,7 @@
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var ChannelStore = require('../stores/channel_store.jsx')
var UserStore = require('../stores/user_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -395,14 +396,10 @@ module.exports.textToJsx = function(text, options) {
var inner = [];
// Function specific regexes
var hashRegex = /^href="#[^"]+"|(#[A-Za-z]+[A-Za-z0-9_]*[A-Za-z0-9])$/g;
// Function specific regex
var hashRegex = /^href="#[^"]+"|(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$/g;
var implicitKeywords = {};
var keywordArray = UserStore.getCurrentMentionKeys();
for (var i = 0; i < keywordArray.length; i++) {
implicitKeywords[keywordArray[i]] = true;
}
var implicitKeywords = UserStore.getCurrentMentionKeys();
var lines = text.split("\n");
var urlMatcher = new LinkifyIt();
@@ -421,10 +418,13 @@ module.exports.textToJsx = function(text, options) {
highlightSearchClass = " search-highlight";
}
if (explicitMention && UserStore.getProfileByUsername(explicitMention[1])) {
if (explicitMention &&
(UserStore.getProfileByUsername(explicitMention[1]) ||
Constants.SPECIAL_MENTIONS.indexOf(explicitMention[1]) !== -1))
{
var name = explicitMention[1];
// do both a non-case sensitive and case senstive check
var mClass = (name.toLowerCase() in implicitKeywords || name in implicitKeywords) ? mentionClass : "";
var mClass = implicitKeywords.indexOf('@'+name.toLowerCase()) !== -1 || implicitKeywords.indexOf('@'+name) !== -1 ? mentionClass : "";
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
@@ -446,7 +446,7 @@ module.exports.textToJsx = function(text, options) {
} else if (trimWord.match(hashRegex)) {
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
var mClass = trimWord in implicitKeywords || trimWord.toLowerCase() in implicitKeywords ? mentionClass : "";
var mClass = implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1 ? mentionClass : "";
if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) {
highlightSearchClass = " search-highlight";
@@ -454,7 +454,7 @@ module.exports.textToJsx = function(text, options) {
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_hash"} className={"theme " + mClass + highlightSearchClass} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(trimWord)}>{trimWord}</a>{suffix} </span>);
} else if (trimWord in implicitKeywords || trimWord.toLowerCase() in implicitKeywords) {
} else if (implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1) {
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
@@ -727,6 +727,25 @@ module.exports.isComment = function(post) {
return false;
}
module.exports.getDirectTeammate = function(channel_id) {
var userIds = ChannelStore.get(channel_id).name.split('__');
var curUserId = UserStore.getCurrentId();
var teammate = {};
if(userIds.length != 2 || userIds.indexOf(curUserId) === -1) {
return teammate;
}
for (var idx in userIds) {
if(userIds[idx] !== curUserId) {
teammate = UserStore.getProfile(userIds[idx]);
break;
}
}
return teammate;
}
Image.prototype.load = function(url, progressCallback) {
var thisImg = this;
var xmlHTTP = new XMLHttpRequest();

Просмотреть файл

@@ -3,7 +3,7 @@ html, body {
}
body {
font-family: 'Lato', sans-serif;
font-family: 'Open Sans', sans-serif;
-webkit-font-smoothing: antialiased;
background: #e9e9e9;
position: relative;
@@ -11,21 +11,29 @@ body {
&.white {
background: #fff;
.inner__wrap {
height: 100%;
> .row.content {
min-height: 100%;
margin-bottom: -89px;
}
}
.row.content {
min-height: 100%;
height: auto !important;
}
.inner__wrap {
height: 100%;
> .row.main {
height: 100%;
margin-bottom: -89px;
}
}
> .container-fluid {
@include clearfix;
height: 100%;
position: relative;
}
}
b, strong {
font-weight: 600;
}
a {
word-break: break-word;
}
@@ -142,7 +150,7 @@ div.theme {
top: 0;
color: #FFF;
font-size: 20px;
font-weight: bold;
font-weight: 600;
text-decoration: none;
padding: 0 10px;
}

Просмотреть файл

@@ -65,7 +65,7 @@
float:left;
}
.channel-intro-title {
font-weight:bold;
font-weight:600;
}
.channel-intro-text {
margin-top:35px;
@@ -88,7 +88,7 @@
}
.dropdown-menu {
li a {
padding: 3 20px;
padding: 3px 20px;
color: #555;
}
}

68
web/sass-files/sass/partials/_loading.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,68 @@
.loading-screen {
display: table;
width: 100%;
height: 100%;
position: absolute;
@include box-sizing(border-box);
text-align: center;
.loading__content {
display: table-cell;
vertical-align: middle;
h3 {
font-weight: 400;
margin: 0 0.2em 0;
display: inline-block;
}
}
}
.loading-screen {
.loading__content {
.round {
background-color: #444;
width: 4px;
height: 4px;
display: inline-block;
margin: 0 1px;
opacity: 0.1;
@include border-radius(10px);
-moz-animation: move 0.75s infinite linear;
-webkit-animation: move 0.75s infinite linear;
}
#round_1 {
-moz-animation-delay: .2s;
-webkit-animation-delay: .2s;
}
#round_2 {
-moz-animation-delay: .4s;
-webkit-animation-delay: .4s;
}
#round_3 {
-moz-animation-delay: .6s;
-webkit-animation-delay: .6s;
}
@-moz-keyframes move {
0% {
opacity: 1;
}
100% {
opacity: 0.1;
};
}
@-webkit-keyframes move {
0% {
opacity: 1;
}
100% {
opacity: 0.1;
};
}
}
}

Просмотреть файл

@@ -3,21 +3,19 @@
background: $primary-color;
position: relative;
z-index: 10;
padding-bottom: 1px;
padding-bottom: 2px;
@include border-radius(3px);
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.mentions--top {
position: absolute;
z-index:99999;
z-index: 1060;
.mentions-box {
position:absolute;
background-color:#fff;
border:1px solid #ddd;
overflow:scroll;
border: $border-gray;
overflow-x: hidden;
overflow-y: scroll;
bottom:0;
}
}
@@ -29,10 +27,10 @@
height:37px;
padding:2px;
z-index:101;
}
.mentions-name:hover {
background-color:#e8eaed;
cursor: pointer;
&:hover {
background-color:#e8eaed;
}
}
.mentions-text {
@@ -46,6 +44,11 @@
border-radius: 10%;
}
.mention-fullname {
color: grey;
padding-left: 10px;
}
.mention-highlight {
background-color:#fff2bb;
}
@@ -53,3 +56,8 @@
.mention-link {
color:$primary-color;
}
.mention-align {
position:relative;
top:5px;
}

Просмотреть файл

@@ -1,9 +1,17 @@
.modal-body {
padding: 20px 15px;
}
.modal {
&.image_modal {
.modal-backdrop.in {
@include opacity(0.7);
}
}
.info__label {
font-weight: 600;
text-align: right;
padding-right: 0;
}
.remove__member {
float: right;
}
@@ -13,7 +21,6 @@
margin-right: auto;
}
.modal-body {
max-height: 75%;
overflow: auto;
}
.modal-push-down {
@@ -29,7 +36,7 @@
border-radius: 0;
background: $primary-color;
color: #FFF;
padding: 15px 15px 11px;
padding: 15px 15px 11px;
border: none;
min-height: 56px;
@include clearfix;
@@ -41,14 +48,23 @@
margin: 0;
}
button.close {
margin-top: 0;
margin: -2px -2px 0 0;
color: #fff;
@include opacity(1);
z-index: 5;
width: 30px;
height: 30px;
line-height: 30px;
@include single-transition(all, 0.25s, ease-in);
position: relative;
&:hover {
background: rgba(0, 0, 0, 0.1);
}
span {
line-height: 10px;
}
}
.btn {
margin-right: 10px;
&.btn-primary {
float: right;
margin-top: -4px;
@@ -102,7 +118,7 @@
}
.more-channel-name {
color: #444;
font-weight: bold;
font-weight: 600;
font-size: 0.95em;
}
tbody {
@@ -127,17 +143,19 @@
.modal-image {
position:relative;
width:100%;
height: 100%;
margin: 0 auto;
.image-wrapper {
padding: 4px;
background: #FFF;
position: relative;
max-width: 80%;
min-height: 280px;
min-width: 280px;
@include border-radius(4px);
@include border-radius(3px);
display: table;
margin: 0 auto;
&:hover {
@include border-radius(3px 3px 0 0);
}
&:hover .modal-close {
@include opacity(1);
}
@@ -217,10 +235,11 @@
}
.modal-button-bar {
position:absolute;
bottom:0px;
bottom:-40px;
left:0px;
right:0px;
background-color:rgba(0, 0, 0, 0.8);
background-color: #222;
@include border-radius(0 0 3px 3px);
@include opacity(0);
-webkit-transition: opacity 0.6s;
-moz-transition: opacity 0.6s;
@@ -228,7 +247,6 @@
transition: opacity 0.6s;
line-height: 40px;
padding: 0 10px;
margin: 4px;
&.footer--show {
@include opacity(1);
}

Просмотреть файл

@@ -86,10 +86,11 @@ body.ios {
background: #FFF;
display: inline-block;
padding: 0 1em;
font-weight: 900;
font-weight: 700;
@include border-radius(50px);
position: relative;
z-index: 5;
font-size: 13px;
}
}
.new-separator {
@@ -212,6 +213,12 @@ body.ios {
}
}
}
&.current--user {
.post-body {
@include border-radius(0 4px 4px 0);
background: #f5f5f5;
}
}
&.same--root {
.comment-icon__container {
@include opacity(0);
@@ -249,6 +256,7 @@ body.ios {
}
p {
margin: 0 0 5px;
font-size: 0.97em;
white-space: pre-wrap;
}
.comment-icon__container {
@@ -269,7 +277,7 @@ body.ios {
float: left;
.post-profile-img {
margin-right: 10px;
@include border-radius(3px);
@include border-radius(50px);
}
}
&.post__content {
@@ -290,6 +298,8 @@ body.ios {
width: 600px;
float: left;
word-wrap: break-word;
padding: 0.3em 0.5em 0.1em;
margin: -0.3em 0 0;
.post-link {
@include clearfix;
text-overflow: ellipsis;
@@ -359,7 +369,7 @@ body.ios {
.embed-title {
margin: 3px 0 1px;
color: #555;
font-weight: bold;
font-weight: 600;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;

Просмотреть файл

@@ -5,7 +5,7 @@
&.post--comment {
&.other--root {
.post-comment {
margin-left: 0;
margin-left: -7px;
}
}
}
@@ -22,11 +22,16 @@
width: 825px;
}
.post-body {
padding-left: 0;
width: 736px;
border: none;
margin: 0;
}
}
.post-body {
float: none;
width: 750px;
margin: 0;
}
.post__content {
width: 920px;
}
@@ -63,10 +68,6 @@
}
}
}
.post-body {
float: none;
width: 750px;
}
}
}
}
@@ -81,7 +82,7 @@
&.post--comment {
&.other--root {
.post-comment {
margin-left: 0;
margin-left: -7px;
}
}
}
@@ -89,6 +90,7 @@
margin-left: 60px;
padding-left: 10px;
border-left: 4px solid #EEE;
margin-bottom: 10px;
div.post-profile-img__container {
.post-profile-img {
display: none;
@@ -98,7 +100,7 @@
width: 825px;
}
.post-body {
padding-left: 0;
width: 736px;
border: none;
margin: 0;
}
@@ -135,6 +137,7 @@
}
}
.post-body {
margin: 0;
float: none;
width: 750px;
}
@@ -231,17 +234,14 @@
}
}
.modal {
.info__label {
text-align: left;
padding-bottom: 5px;
}
.modal-header {
padding-left: 20px;
padding-right: 20px;
.modal-action {
margin-top: 10px;
}
button.close {
width: 35px;
height: 32px;
margin: -5px -10px 0;
}
.modal-title {
float: none;
}
@@ -261,9 +261,11 @@
.settings-table {
display: block;
.settings-content {
display: block;
.section-edit {
text-align: left;
&.minimize-settings {
display: block;
.section-edit {
text-align: left;
}
}
}
.settings-links {
@@ -287,10 +289,12 @@
}
.settings-table {
.settings-content {
padding: 0;
display: none;
.user-settings {
padding: 70px 20px 30px;
&.minimize-settings {
padding: 0;
display: none;
.user-settings {
padding: 70px 20px 30px;
}
}
}
.settings-links {
@@ -538,7 +542,6 @@
.modal {
.modal-image {
.image-wrapper {
padding-bottom: 40px;
.modal-close {
@include opacity(1);
}
@@ -575,7 +578,7 @@
&.post--comment {
&.other--root {
.post-comment {
margin-left: 11px;
margin-left: 4px;
}
}
}

Просмотреть файл

@@ -77,7 +77,7 @@
border: none;
}
.search-channel__name {
font-weight: bold;
font-weight: 600;
margin: 0 0 10px 0;
}
}

Просмотреть файл

@@ -56,7 +56,7 @@
.section-title {
margin-bottom: 5px;
font-weight: bold;
font-weight: 600;
}
.section-edit {
@@ -172,7 +172,7 @@
border-top:1px solid lightgrey;
}
.post-profile-img {
@include border-radius(3px);
@include border-radius(50px);
margin-right: 8px;
}
.member-name {

Просмотреть файл

@@ -59,7 +59,7 @@
color: #999;
&.unread-title {
color: #333;
font-weight: bold;
font-weight: 600;
}
&:hover, &:focus {
background: #e6f2fa;
@@ -70,6 +70,7 @@
color: #111;
background-color: #e1e1e1;
border-radius: 0;
font-weight: 400;
}
}
}

Просмотреть файл

@@ -29,6 +29,7 @@
@import "partials/modal";
@import "partials/mentions";
@import "partials/error";
@import "partials/loading";
// Responsive Css
@import "partials/responsive";

Просмотреть файл

@@ -1,4 +1,6 @@
{{define "channel"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body>

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "find_team"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">

Просмотреть файл

@@ -14,7 +14,7 @@
<link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">
<link href='https://fonts.googleapis.com/css?family=Lato:400,700,900' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,600,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/styles.css">
<script src="/static/js/min/perfect-scrollbar.min.js"></script>

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "home"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body>

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "login"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "password_reset"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "signup_team"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "signup_team_complete"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "signup_team_confirm"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "signup_user_complete"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "verify"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body>

Просмотреть файл

@@ -1,4 +1,5 @@
{{define "welcome"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body>

Просмотреть файл

@@ -285,7 +285,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
otherUserId = ids[0]
}
if sc, err := api.CreateDirectChannel(c, otherUserId, r.URL.Path); err != nil {
if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil {
api.Handle404(w, r)
return
} else {