summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/team.go33
-rw-r--r--api/team_test.go40
-rw-r--r--api/user.go65
-rw-r--r--api/user_test.go39
-rw-r--r--doc/help/Messaging.md2
-rw-r--r--mattermost.go128
-rw-r--r--store/sql_audit_store.go19
-rw-r--r--store/sql_audit_store_test.go4
-rw-r--r--store/sql_channel_store.go34
-rw-r--r--store/sql_channel_store_test.go57
-rw-r--r--store/sql_oauth_store.go18
-rw-r--r--store/sql_oauth_store_test.go14
-rw-r--r--store/sql_post_store.go93
-rw-r--r--store/sql_post_store_test.go70
-rw-r--r--store/sql_preference_store.go18
-rw-r--r--store/sql_preference_store_test.go40
-rw-r--r--store/sql_session_store.go18
-rw-r--r--store/sql_session_store_test.go23
-rw-r--r--store/sql_team_store.go17
-rw-r--r--store/sql_team_store_test.go23
-rw-r--r--store/sql_user_store.go18
-rw-r--r--store/sql_user_store_test.go13
-rw-r--r--store/sql_webhook_store.go36
-rw-r--r--store/sql_webhook_store_test.go57
-rw-r--r--store/store.go11
-rw-r--r--utils/config.go7
-rw-r--r--web/react/components/channel_header.jsx38
-rw-r--r--web/react/components/create_comment.jsx3
-rw-r--r--web/react/components/create_post.jsx7
-rw-r--r--web/react/components/delete_post_modal.jsx155
-rw-r--r--web/react/components/edit_post_modal.jsx3
-rw-r--r--web/react/components/file_preview.jsx21
-rw-r--r--web/react/components/post.jsx61
-rw-r--r--web/react/components/post_body.jsx44
-rw-r--r--web/react/components/post_header.jsx8
-rw-r--r--web/react/components/post_info.jsx55
-rw-r--r--web/react/components/rhs_comment.jsx71
-rw-r--r--web/react/components/rhs_root_post.jsx86
-rw-r--r--web/react/components/search_results_item.jsx41
-rw-r--r--web/react/components/time_since.jsx2
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx5
-rw-r--r--web/react/stores/modal_store.jsx8
-rw-r--r--web/react/stores/post_store.jsx19
-rw-r--r--web/react/utils/channel_intro_mssages.jsx1
-rw-r--r--web/react/utils/constants.jsx3
-rw-r--r--web/react/utils/utils.jsx39
-rw-r--r--web/sass-files/sass/partials/_base.scss1
-rw-r--r--web/sass-files/sass/partials/_headers.scss32
-rw-r--r--web/sass-files/sass/partials/_markdown.scss2
-rw-r--r--web/sass-files/sass/partials/_post.scss499
-rw-r--r--web/sass-files/sass/partials/_post_right.scss33
-rw-r--r--web/sass-files/sass/partials/_responsive.scss331
-rw-r--r--web/sass-files/sass/partials/_search.scss2
-rw-r--r--web/sass-files/sass/partials/_sidebar--right.scss14
54 files changed, 1576 insertions, 905 deletions
diff --git a/api/team.go b/api/team.go
index 862970887..2cc7106dc 100644
--- a/api/team.go
+++ b/api/team.go
@@ -582,6 +582,39 @@ func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(oldTeam.ToJson()))
}
+func PermanentDeleteTeam(c *Context, team *model.Team) *model.AppError {
+ l4g.Warn("Attempting to permanently delete team %v id=%v", team.Name, team.Id)
+ c.Path = "/teams/permanent_delete"
+ c.LogAuditWithUserId("", fmt.Sprintf("attempt teamId=%v", team.Id))
+
+ team.DeleteAt = model.GetMillis()
+ if result := <-Srv.Store.Team().Update(team); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.User().GetForExport(team.Id); result.Err != nil {
+ return result.Err
+ } else {
+ users := result.Data.([]*model.User)
+ for _, user := range users {
+ PermanentDeleteUser(c, user)
+ }
+ }
+
+ if result := <-Srv.Store.Channel().PermanentDeleteByTeam(team.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Team().PermanentDelete(team.Id); result.Err != nil {
+ return result.Err
+ }
+
+ l4g.Warn("Permanently deleted team %v id=%v", team.Name, team.Id)
+ c.LogAuditWithUserId("", fmt.Sprintf("success teamId=%v", team.Id))
+
+ return nil
+}
+
func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) {
if len(c.Session.TeamId) == 0 {
diff --git a/api/team_test.go b/api/team_test.go
index 7a3b092ce..0b7d2ed9c 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -168,6 +168,45 @@ func TestGetAllTeams(t *testing.T) {
}
}
+func TestTeamPermDelete(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "search for post1"}
+ post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+
+ post2 := &model.Post{ChannelId: channel1.Id, Message: "search for post2"}
+ post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
+
+ post3 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag search for post3"}
+ post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post)
+
+ post4 := &model.Post{ChannelId: channel1.Id, Message: "hashtag for post4"}
+ post4 = Client.Must(Client.CreatePost(post4)).Data.(*model.Post)
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "test"
+
+ err := PermanentDeleteTeam(c, team)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ Client.ClearOAuthToken()
+}
+
/*
XXXXXX investigate and fix failing test
@@ -221,6 +260,7 @@ func TestFindTeamByEmailSend(t *testing.T) {
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.FindTeamsSendEmail(user.Email); err != nil {
t.Fatal(err)
diff --git a/api/user.go b/api/user.go
index 0f868a678..3281e83e2 100644
--- a/api/user.go
+++ b/api/user.go
@@ -1196,6 +1196,14 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+ ruser := UpdateActive(c, user, active)
+
+ if c.Err == nil {
+ w.Write([]byte(ruser.ToJson()))
+ }
+}
+
+func UpdateActive(c *Context, user *model.User, active bool) *model.User {
if active {
user.DeleteAt = 0
} else {
@@ -1204,7 +1212,7 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) {
if result := <-Srv.Store.User().Update(user, true); result.Err != nil {
c.Err = result.Err
- return
+ return nil
} else {
c.LogAuditWithUserId(user.Id, fmt.Sprintf("active=%v", active))
@@ -1216,8 +1224,61 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) {
options := utils.SanitizeOptions
options["passwordupdate"] = false
ruser.Sanitize(options)
- w.Write([]byte(ruser.ToJson()))
+ return ruser
+ }
+}
+
+func PermanentDeleteUser(c *Context, user *model.User) *model.AppError {
+ l4g.Warn("Attempting to permanently delete account %v id=%v", user.Email, user.Id)
+ c.Path = "/users/permanent_delete"
+ c.LogAuditWithUserId(user.Id, fmt.Sprintf("attempt userId=%v", user.Id))
+ c.LogAuditWithUserId("", fmt.Sprintf("attempt userId=%v", user.Id))
+ if user.IsInRole(model.ROLE_SYSTEM_ADMIN) {
+ l4g.Warn("You are deleting %v that is a system administrator. You may need to set another account as the system administrator using the command line tools.", user.Email)
+ }
+
+ UpdateActive(c, user, false)
+
+ if result := <-Srv.Store.Session().PermanentDeleteSessionsByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); result.Err != nil {
+ return result.Err
}
+
+ if result := <-Srv.Store.Webhook().PermanentDeleteIncomingByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Webhook().PermanentDeleteOutgoingByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Preference().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Channel().PermanentDeleteMembersByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Post().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.User().PermanentDelete(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Audit().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ l4g.Warn("Permanently deleted account %v id=%v", user.Email, user.Id)
+ c.LogAuditWithUserId("", fmt.Sprintf("success userId=%v", user.Id))
+
+ return nil
}
func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
diff --git a/api/user_test.go b/api/user_test.go
index f067182cb..63a1e337b 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -767,6 +767,45 @@ func TestUserUpdateActive(t *testing.T) {
}
}
+func TestUserPermDelete(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "search for post1"}
+ post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+
+ post2 := &model.Post{ChannelId: channel1.Id, Message: "search for post2"}
+ post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
+
+ post3 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag search for post3"}
+ post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post)
+
+ post4 := &model.Post{ChannelId: channel1.Id, Message: "hashtag for post4"}
+ post4 = Client.Must(Client.CreatePost(post4)).Data.(*model.Post)
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "test"
+
+ err := PermanentDeleteUser(c, user1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ Client.ClearOAuthToken()
+}
+
func TestSendPasswordReset(t *testing.T) {
Setup()
diff --git a/doc/help/Messaging.md b/doc/help/Messaging.md
index 03adc1ce8..09d35f3b9 100644
--- a/doc/help/Messaging.md
+++ b/doc/help/Messaging.md
@@ -4,7 +4,7 @@
You can write messages using the input box with the text "Write a message..." at the bottom of Mattermost.
-Press **ENTER** to send a message. Use **Ctrl+ENTER** to create a new line without sending a message.
+Press **ENTER** to send a message. Use **Shift+ENTER** to create a new line without sending a message.
## Formatting Messages
diff --git a/mattermost.go b/mattermost.go
index e1ae58904..2d5727400 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -30,6 +30,8 @@ var flagCmdCreateUser bool
var flagCmdAssignRole bool
var flagCmdVersion bool
var flagCmdResetPassword bool
+var flagCmdPermanentDeleteUser bool
+var flagCmdPermanentDeleteTeam bool
var flagConfigFile string
var flagEmail string
var flagPassword string
@@ -191,10 +193,18 @@ func parseCmds() {
flag.BoolVar(&flagCmdAssignRole, "assign_role", false, "")
flag.BoolVar(&flagCmdVersion, "version", false, "")
flag.BoolVar(&flagCmdResetPassword, "reset_password", false, "")
+ flag.BoolVar(&flagCmdPermanentDeleteUser, "permanent_delete_user", false, "")
+ flag.BoolVar(&flagCmdPermanentDeleteTeam, "permanent_delete_team", false, "")
flag.Parse()
- flagRunCmds = flagCmdCreateTeam || flagCmdCreateUser || flagCmdAssignRole || flagCmdResetPassword || flagCmdVersion
+ flagRunCmds = (flagCmdCreateTeam ||
+ flagCmdCreateUser ||
+ flagCmdAssignRole ||
+ flagCmdResetPassword ||
+ flagCmdVersion ||
+ flagCmdPermanentDeleteUser ||
+ flagCmdPermanentDeleteTeam)
}
func runCmds() {
@@ -203,6 +213,8 @@ func runCmds() {
cmdCreateUser()
cmdAssignRole()
cmdResetPassword()
+ cmdPermDeleteUser()
+ cmdPermDeleteTeam()
}
func cmdCreateTeam() {
@@ -406,6 +418,106 @@ func cmdResetPassword() {
}
}
+func cmdPermDeleteUser() {
+ if flagCmdPermanentDeleteUser {
+ if len(flagTeamName) == 0 {
+ fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name")
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ if len(flagEmail) == 0 {
+ fmt.Fprintln(os.Stderr, "flag needs an argument: -email")
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ c := &api.Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+
+ var team *model.Team
+ if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ l4g.Error("%v", result.Err)
+ flushLogAndExit(1)
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-api.Srv.Store.User().GetByEmail(team.Id, flagEmail); result.Err != nil {
+ l4g.Error("%v", result.Err)
+ flushLogAndExit(1)
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ var confirmBackup string
+ fmt.Print("Have you performed a database backup? (YES/NO): ")
+ fmt.Scanln(&confirmBackup)
+ if confirmBackup != "YES" {
+ flushLogAndExit(1)
+ }
+
+ var confirm string
+ fmt.Printf("Are you sure you want to delete the user %v? All data will be permanently deleted? (YES/NO): ", user.Email)
+ fmt.Scanln(&confirm)
+ if confirm != "YES" {
+ flushLogAndExit(1)
+ }
+
+ if err := api.PermanentDeleteUser(c, user); err != nil {
+ l4g.Error("%v", err)
+ flushLogAndExit(1)
+ } else {
+ flushLogAndExit(0)
+ }
+ }
+}
+
+func cmdPermDeleteTeam() {
+ if flagCmdPermanentDeleteTeam {
+ if len(flagTeamName) == 0 {
+ fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name")
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ c := &api.Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+
+ var team *model.Team
+ if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ l4g.Error("%v", result.Err)
+ flushLogAndExit(1)
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var confirmBackup string
+ fmt.Print("Have you performed a database backup? (YES/NO): ")
+ fmt.Scanln(&confirmBackup)
+ if confirmBackup != "YES" {
+ flushLogAndExit(1)
+ }
+
+ var confirm string
+ fmt.Printf("Are you sure you want to delete the team %v? All data will be permanently deleted? (YES/NO): ", team.Name)
+ fmt.Scanln(&confirm)
+ if confirm != "YES" {
+ flushLogAndExit(1)
+ }
+
+ if err := api.PermanentDeleteTeam(c, team); err != nil {
+ l4g.Error("%v", err)
+ flushLogAndExit(1)
+ } else {
+ flushLogAndExit(0)
+ }
+ }
+}
+
func flushLogAndExit(code int) {
l4g.Close()
time.Sleep(time.Second)
@@ -461,5 +573,19 @@ Usage:
Example:
platform -reset_password -team_name="name" -email="user@example.com" -password="newpassword"
+ -permanent_delete_user Permanently deletes a user and all related information
+ include posts from the database. It requires the
+ -team_name, and -email flag. You may need to restart the
+ server to invlidate the cache
+ Example:
+ platform -permanent_delete_user -team_name="name" -email="user@example.com"
+
+ -permanent_delete_team Permanently deletes a team and all users along with
+ all related information including posts from the database.
+ It requires the -team_name flag. You may need to restart
+ the server to invalidate the cache.
+ Example:
+ platform -permanent_delete_team -team_name="name"
+
`
diff --git a/store/sql_audit_store.go b/store/sql_audit_store.go
index b3e2daea0..f4fd29aab 100644
--- a/store/sql_audit_store.go
+++ b/store/sql_audit_store.go
@@ -86,3 +86,22 @@ func (s SqlAuditStore) Get(user_id string, limit int) StoreChannel {
return storeChannel
}
+
+func (s SqlAuditStore) PermanentDeleteByUser(userId string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := s.GetMaster().Exec("DELETE FROM Audits WHERE UserId = :userId",
+ map[string]interface{}{"userId": userId}); err != nil {
+ result.Err = model.NewAppError("SqlAuditStore.Delete", "We encountered an error deleting the audits", "user_id="+userId)
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_audit_store_test.go b/store/sql_audit_store_test.go
index e265db837..b395631f1 100644
--- a/store/sql_audit_store_test.go
+++ b/store/sql_audit_store_test.go
@@ -44,4 +44,8 @@ func TestSqlAuditStore(t *testing.T) {
if len(audits) != 0 {
t.Fatal("Should have returned empty because user_id is missing")
}
+
+ if r2 := <-store.Audit().PermanentDeleteByUser(audit.UserId); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
}
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index a9f99bd67..badaa4d13 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -279,6 +279,23 @@ func (s SqlChannelStore) Delete(channelId string, time int64) StoreChannel {
return storeChannel
}
+func (s SqlChannelStore) PermanentDeleteByTeam(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := s.GetMaster().Exec("DELETE FROM Channels WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.PermanentDeleteByTeam", "We couldn't delete the channels", "teamId="+teamId+", "+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
type channelWithMember struct {
model.Channel
model.ChannelMember
@@ -616,6 +633,23 @@ func (s SqlChannelStore) RemoveMember(channelId string, userId string) StoreChan
return storeChannel
}
+func (s SqlChannelStore) PermanentDeleteMembersByUser(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := s.GetMaster().Exec("DELETE FROM ChannelMembers WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.RemoveMember", "We couldn't remove the channel member", "user_id="+userId+", "+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlChannelStore) CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go
index 8662fcbd3..695991bf7 100644
--- a/store/sql_channel_store_test.go
+++ b/store/sql_channel_store_test.go
@@ -379,6 +379,63 @@ func TestChannelMemberStore(t *testing.T) {
}
}
+func TestChannelDeleteMemberStore(t *testing.T) {
+ Setup()
+
+ c1 := model.Channel{}
+ c1.TeamId = model.NewId()
+ c1.DisplayName = "NameName"
+ c1.Name = "a" + model.NewId() + "b"
+ c1.Type = model.CHANNEL_OPEN
+ c1 = *Must(store.Channel().Save(&c1)).(*model.Channel)
+
+ c1t1 := (<-store.Channel().Get(c1.Id)).Data.(*model.Channel)
+ t1 := c1t1.ExtraUpdateAt
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ u1.Nickname = model.NewId()
+ Must(store.User().Save(&u1))
+
+ u2 := model.User{}
+ u2.TeamId = model.NewId()
+ u2.Email = model.NewId()
+ u2.Nickname = model.NewId()
+ Must(store.User().Save(&u2))
+
+ o1 := model.ChannelMember{}
+ o1.ChannelId = c1.Id
+ o1.UserId = u1.Id
+ o1.NotifyProps = model.GetDefaultChannelNotifyProps()
+ Must(store.Channel().SaveMember(&o1))
+
+ o2 := model.ChannelMember{}
+ o2.ChannelId = c1.Id
+ o2.UserId = u2.Id
+ o2.NotifyProps = model.GetDefaultChannelNotifyProps()
+ Must(store.Channel().SaveMember(&o2))
+
+ c1t2 := (<-store.Channel().Get(c1.Id)).Data.(*model.Channel)
+ t2 := c1t2.ExtraUpdateAt
+
+ if t2 <= t1 {
+ t.Fatal("Member update time incorrect")
+ }
+
+ count := (<-store.Channel().GetMemberCount(o1.ChannelId)).Data.(int64)
+ if count != 2 {
+ t.Fatal("should have saved 2 members")
+ }
+
+ Must(store.Channel().PermanentDeleteMembersByUser(o2.UserId))
+
+ count = (<-store.Channel().GetMemberCount(o1.ChannelId)).Data.(int64)
+ if count != 1 {
+ t.Fatal("should have removed 1 member")
+ }
+}
+
func TestChannelStorePermissionsTo(t *testing.T) {
Setup()
diff --git a/store/sql_oauth_store.go b/store/sql_oauth_store.go
index 751207b85..43a5bee31 100644
--- a/store/sql_oauth_store.go
+++ b/store/sql_oauth_store.go
@@ -332,3 +332,21 @@ func (as SqlOAuthStore) RemoveAuthData(code string) StoreChannel {
return storeChannel
}
+
+func (as SqlOAuthStore) PermanentDeleteAuthDataByUser(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := as.GetMaster().Exec("DELETE FROM OAuthAuthData WHERE UserId = :UserId", map[string]interface{}{"UserId": userId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlOAuthStore.RemoveAuthDataByUserId", "We couldn't remove the authorization code", "err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_oauth_store_test.go b/store/sql_oauth_store_test.go
index 3f05f1c92..c3f6ea7ac 100644
--- a/store/sql_oauth_store_test.go
+++ b/store/sql_oauth_store_test.go
@@ -180,3 +180,17 @@ func TestOAuthStoreRemoveAuthData(t *testing.T) {
t.Fatal("should have errored - auth code removed")
}
}
+
+func TestOAuthStoreRemoveAuthDataByUser(t *testing.T) {
+ Setup()
+
+ a1 := model.AuthData{}
+ a1.ClientId = model.NewId()
+ a1.UserId = model.NewId()
+ a1.Code = model.NewId()
+ Must(store.OAuth().SaveAuthData(&a1))
+
+ if err := (<-store.OAuth().PermanentDeleteAuthDataByUser(a1.UserId)).Err; err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index f800367cb..cc596074f 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -228,6 +228,99 @@ func (s SqlPostStore) Delete(postId string, time int64) StoreChannel {
return storeChannel
}
+func (s SqlPostStore) permanentDelete(postId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("DELETE FROM Posts WHERE Id = :Id OR ParentId = :ParentId OR RootId = :RootId", map[string]interface{}{"Id": postId, "ParentId": postId, "RootId": postId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.Delete", "We couldn't delete the post", "id="+postId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) permanentDeleteAllCommentByUser(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("DELETE FROM Posts WHERE UserId = :UserId AND RootId != ''", map[string]interface{}{"UserId": userId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.permanentDeleteAllCommentByUser", "We couldn't delete the comments for user", "userId="+userId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) PermanentDeleteByUser(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ // First attempt to delete all the comments for a user
+ if r := <-s.permanentDeleteAllCommentByUser(userId); r.Err != nil {
+ result.Err = r.Err
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ // Now attempt to delete all the root posts for a user. This will also
+ // delete all the comments for each post.
+ found := true
+ count := 0
+
+ for found {
+ var ids []string
+ _, err := s.GetMaster().Select(&ids, "SELECT Id FROM Posts WHERE UserId = :UserId LIMIT 1000", map[string]interface{}{"UserId": userId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.PermanentDeleteByUser.select", "We couldn't select the posts to delete for the user", "userId="+userId+", err="+err.Error())
+ storeChannel <- result
+ close(storeChannel)
+ return
+ } else {
+ found = false
+ for _, id := range ids {
+ found = true
+ if r := <-s.permanentDelete(id); r.Err != nil {
+ result.Err = r.Err
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+ }
+ }
+
+ // This is a fail safe, give up if more than 10K messages
+ count = count + 1
+ if count >= 10 {
+ result.Err = model.NewAppError("SqlPostStore.PermanentDeleteByUser.toolarge", "We couldn't select the posts to delete for the user (too many), please re-run", "userId="+userId)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlPostStore) GetPosts(channelId string, offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index fe7195a54..d9b087ea7 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -247,6 +247,76 @@ func TestPostStoreDelete2Level(t *testing.T) {
}
}
+func TestPostStorePermDelete1Level(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ o2 := &model.Post{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = model.NewId()
+ o2.Message = "a" + model.NewId() + "b"
+ o2.ParentId = o1.Id
+ o2.RootId = o1.Id
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+
+ if r2 := <-store.Post().PermanentDeleteByUser(o2.UserId); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Post().Get(o1.Id)); r3.Err != nil {
+ t.Fatal("Deleted id shouldn't have failed")
+ }
+
+ if r4 := (<-store.Post().Get(o2.Id)); r4.Err == nil {
+ t.Fatal("Deleted id should have failed")
+ }
+}
+
+func TestPostStorePermDelete1Level2(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ o2 := &model.Post{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = model.NewId()
+ o2.Message = "a" + model.NewId() + "b"
+ o2.ParentId = o1.Id
+ o2.RootId = o1.Id
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+
+ o3 := &model.Post{}
+ o3.ChannelId = model.NewId()
+ o3.UserId = model.NewId()
+ o3.Message = "a" + model.NewId() + "b"
+ o3 = (<-store.Post().Save(o3)).Data.(*model.Post)
+
+ if r2 := <-store.Post().PermanentDeleteByUser(o1.UserId); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Post().Get(o1.Id)); r3.Err == nil {
+ t.Fatal("Deleted id should have failed")
+ }
+
+ if r4 := (<-store.Post().Get(o2.Id)); r4.Err == nil {
+ t.Fatal("Deleted id should have failed")
+ }
+
+ if r5 := (<-store.Post().Get(o3.Id)); r5.Err != nil {
+ t.Fatal("Deleted id shouldn't have failed")
+ }
+}
+
func TestPostStoreGetWithChildren(t *testing.T) {
Setup()
diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go
index f9f38b747..8454abcbd 100644
--- a/store/sql_preference_store.go
+++ b/store/sql_preference_store.go
@@ -239,3 +239,21 @@ func (s SqlPreferenceStore) GetAll(userId string) StoreChannel {
return storeChannel
}
+
+func (s SqlPreferenceStore) PermanentDeleteByUser(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := s.GetMaster().Exec(
+ `DELETE FROM Preferences WHERE UserId = :UserId`, map[string]interface{}{"UserId": userId}); err != nil {
+ result.Err = model.NewAppError("SqlPreferenceStore.Delete", "We encountered an error while deleteing preferences", err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go
index e68203cc3..77da71fd6 100644
--- a/store/sql_preference_store_test.go
+++ b/store/sql_preference_store_test.go
@@ -192,3 +192,43 @@ func TestPreferenceGetAll(t *testing.T) {
}
}
}
+
+func TestPreferenceDelete(t *testing.T) {
+ Setup()
+
+ userId := model.NewId()
+ category := model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW
+ name := model.NewId()
+
+ preferences := model.Preferences{
+ {
+ UserId: userId,
+ Category: category,
+ Name: name,
+ },
+ // same user/category, different name
+ {
+ UserId: userId,
+ Category: category,
+ Name: model.NewId(),
+ },
+ // same user/name, different category
+ {
+ UserId: userId,
+ Category: model.NewId(),
+ Name: name,
+ },
+ // same name/category, different user
+ {
+ UserId: model.NewId(),
+ Category: category,
+ Name: name,
+ },
+ }
+
+ Must(store.Preference().Save(&preferences))
+
+ if result := <-store.Preference().PermanentDeleteByUser(userId); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+}
diff --git a/store/sql_session_store.go b/store/sql_session_store.go
index 27b34ee39..86604b4fe 100644
--- a/store/sql_session_store.go
+++ b/store/sql_session_store.go
@@ -158,6 +158,24 @@ func (me SqlSessionStore) RemoveAllSessionsForTeam(teamId string) StoreChannel {
return storeChannel
}
+func (me SqlSessionStore) PermanentDeleteSessionsByUser(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE UserId = :UserId", map[string]interface{}{"UserId": userId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.RemoveAllSessionsForUser", "We couldn't remove all the sessions for the user", "id="+userId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (me SqlSessionStore) CleanUpExpiredSessions(userId string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go
index 068e5fc19..cec8e93b0 100644
--- a/store/sql_session_store_test.go
+++ b/store/sql_session_store_test.go
@@ -103,6 +103,29 @@ func TestSessionRemoveAll(t *testing.T) {
}
}
+func TestSessionRemoveByUser(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ Must(store.Session().Save(&s1))
+
+ if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil {
+ t.Fatal(rs1.Err)
+ } else {
+ if rs1.Data.(*model.Session).Id != s1.Id {
+ t.Fatal("should match")
+ }
+ }
+
+ Must(store.Session().PermanentDeleteSessionsByUser(s1.UserId))
+
+ if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil {
+ t.Fatal("should have been removed")
+ }
+}
+
func TestSessionRemoveToken(t *testing.T) {
Setup()
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 1a0aeabde..9578549ca 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -300,3 +300,20 @@ func (s SqlTeamStore) GetAllTeamListing() StoreChannel {
return storeChannel
}
+
+func (s SqlTeamStore) PermanentDelete(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := s.GetMaster().Exec("DELETE FROM Teams WHERE Id = :TeamId", map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.Delete", "We couldn't delete the existing team", "teamId="+teamId+", "+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_team_store_test.go b/store/sql_team_store_test.go
index 71740f7e7..7dc31cbe2 100644
--- a/store/sql_team_store_test.go
+++ b/store/sql_team_store_test.go
@@ -238,3 +238,26 @@ func TestAllTeamListing(t *testing.T) {
}
}
}
+
+func TestDelete(t *testing.T) {
+ Setup()
+
+ o1 := model.Team{}
+ o1.DisplayName = "DisplayName"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Email = model.NewId() + "@nowhere.com"
+ o1.Type = model.TEAM_OPEN
+ o1.AllowTeamListing = true
+ Must(store.Team().Save(&o1))
+
+ o2 := model.Team{}
+ o2.DisplayName = "DisplayName"
+ o2.Name = "a" + model.NewId() + "b"
+ o2.Email = model.NewId() + "@nowhere.com"
+ o2.Type = model.TEAM_OPEN
+ Must(store.Team().Save(&o2))
+
+ if r1 := <-store.Team().PermanentDelete(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ }
+}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 77ff5bfab..d19135b64 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -554,3 +554,21 @@ func (us SqlUserStore) GetTotalActiveUsersCount() StoreChannel {
return storeChannel
}
+
+func (us SqlUserStore) PermanentDelete(userId string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := us.GetMaster().Exec("DELETE FROM Users WHERE Id = :UserId", map[string]interface{}{"UserId": userId}); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetByEmail", "We couldn't delete the existing account", "userId="+userId+", "+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go
index 874baf634..dd08438f1 100644
--- a/store/sql_user_store_test.go
+++ b/store/sql_user_store_test.go
@@ -377,3 +377,16 @@ func TestUserStoreUpdatePassword(t *testing.T) {
}
}
}
+
+func TestUserStoreDelete(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ Must(store.User().Save(&u1))
+
+ if err := (<-store.User().PermanentDelete(u1.Id)).Err; err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/store/sql_webhook_store.go b/store/sql_webhook_store.go
index c758e2339..b7bf0615f 100644
--- a/store/sql_webhook_store.go
+++ b/store/sql_webhook_store.go
@@ -116,6 +116,24 @@ func (s SqlWebhookStore) DeleteIncoming(webhookId string, time int64) StoreChann
return storeChannel
}
+func (s SqlWebhookStore) PermanentDeleteIncomingByUser(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("DELETE FROM IncomingWebhooks WHERE UserId = :UserId", map[string]interface{}{"UserId": userId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlWebhookStore.DeleteIncomingByUser", "We couldn't delete the webhook", "id="+userId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlWebhookStore) GetIncomingByUser(userId string) StoreChannel {
storeChannel := make(StoreChannel)
@@ -294,6 +312,24 @@ func (s SqlWebhookStore) DeleteOutgoing(webhookId string, time int64) StoreChann
return storeChannel
}
+func (s SqlWebhookStore) PermanentDeleteOutgoingByUser(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("DELETE FROM OutgoingWebhooks WHERE CreatorId = :UserId", map[string]interface{}{"UserId": userId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlWebhookStore.DeleteOutgoingByUser", "We couldn't delete the webhook", "id="+userId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_webhook_store_test.go b/store/sql_webhook_store_test.go
index 1fb990f3e..1a9d5be3b 100644
--- a/store/sql_webhook_store_test.go
+++ b/store/sql_webhook_store_test.go
@@ -103,6 +103,34 @@ func TestWebhookStoreDeleteIncoming(t *testing.T) {
}
}
+func TestWebhookStoreDeleteIncomingByUser(t *testing.T) {
+ Setup()
+
+ o1 := &model.IncomingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.TeamId = model.NewId()
+
+ o1 = (<-store.Webhook().SaveIncoming(o1)).Data.(*model.IncomingWebhook)
+
+ if r1 := <-store.Webhook().GetIncoming(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.IncomingWebhook).CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned webhook")
+ }
+ }
+
+ if r2 := <-store.Webhook().PermanentDeleteIncomingByUser(o1.UserId); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Webhook().GetIncoming(o1.Id)); r3.Err == nil {
+ t.Log(r3.Data)
+ t.Fatal("Missing id should have failed")
+ }
+}
+
func TestWebhookStoreSaveOutgoing(t *testing.T) {
Setup()
@@ -258,6 +286,35 @@ func TestWebhookStoreDeleteOutgoing(t *testing.T) {
}
}
+func TestWebhookStoreDeleteOutgoingByUser(t *testing.T) {
+ Setup()
+
+ o1 := &model.OutgoingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.CreatorId = model.NewId()
+ o1.TeamId = model.NewId()
+ o1.CallbackURLs = []string{"http://nowhere.com/"}
+
+ o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
+
+ if r1 := <-store.Webhook().GetOutgoing(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.OutgoingWebhook).CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned webhook")
+ }
+ }
+
+ if r2 := <-store.Webhook().PermanentDeleteOutgoingByUser(o1.CreatorId); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Webhook().GetOutgoing(o1.Id)); r3.Err == nil {
+ t.Log(r3.Data)
+ t.Fatal("Missing id should have failed")
+ }
+}
+
func TestWebhookStoreUpdateOutgoing(t *testing.T) {
Setup()
diff --git a/store/store.go b/store/store.go
index 13b59b582..338ae186f 100644
--- a/store/store.go
+++ b/store/store.go
@@ -52,6 +52,7 @@ type TeamStore interface {
GetAll() StoreChannel
GetAllTeamListing() StoreChannel
GetByInviteId(inviteId string) StoreChannel
+ PermanentDelete(teamId string) StoreChannel
}
type ChannelStore interface {
@@ -60,6 +61,7 @@ type ChannelStore interface {
Update(channel *model.Channel) StoreChannel
Get(id string) StoreChannel
Delete(channelId string, time int64) StoreChannel
+ PermanentDeleteByTeam(teamId string) StoreChannel
GetByName(team_id string, domain string) StoreChannel
GetChannels(teamId string, userId string) StoreChannel
GetMoreChannels(teamId string, userId string) StoreChannel
@@ -72,6 +74,7 @@ type ChannelStore interface {
GetMember(channelId string, userId string) StoreChannel
GetMemberCount(channelId string) StoreChannel
RemoveMember(channelId string, userId string) StoreChannel
+ PermanentDeleteMembersByUser(userId string) StoreChannel
GetExtraMembers(channelId string, limit int) StoreChannel
CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel
CheckOpenChannelPermissions(teamId string, channelId string) StoreChannel
@@ -86,6 +89,7 @@ type PostStore interface {
Update(post *model.Post, newMessage string, newHashtags string) StoreChannel
Get(id string) StoreChannel
Delete(postId string, time int64) StoreChannel
+ PermanentDeleteByUser(userId string) StoreChannel
GetPosts(channelId string, offset int, limit int) StoreChannel
GetPostsBefore(channelId string, postId string, numPosts int, offset int) StoreChannel
GetPostsAfter(channelId string, postId string, numPosts int, offset int) StoreChannel
@@ -118,6 +122,7 @@ type UserStore interface {
GetTotalUsersCount() StoreChannel
GetTotalActiveUsersCount() StoreChannel
GetSystemAdminProfiles() StoreChannel
+ PermanentDelete(userId string) StoreChannel
}
type SessionStore interface {
@@ -126,6 +131,7 @@ type SessionStore interface {
GetSessions(userId string) StoreChannel
Remove(sessionIdOrToken string) StoreChannel
RemoveAllSessionsForTeam(teamId string) StoreChannel
+ PermanentDeleteSessionsByUser(teamId string) StoreChannel
UpdateLastActivityAt(sessionId string, time int64) StoreChannel
UpdateRoles(userId string, roles string) StoreChannel
}
@@ -133,6 +139,7 @@ type SessionStore interface {
type AuditStore interface {
Save(audit *model.Audit) StoreChannel
Get(user_id string, limit int) StoreChannel
+ PermanentDeleteByUser(userId string) StoreChannel
}
type OAuthStore interface {
@@ -143,6 +150,7 @@ type OAuthStore interface {
SaveAuthData(authData *model.AuthData) StoreChannel
GetAuthData(code string) StoreChannel
RemoveAuthData(code string) StoreChannel
+ PermanentDeleteAuthDataByUser(userId string) StoreChannel
SaveAccessData(accessData *model.AccessData) StoreChannel
GetAccessData(token string) StoreChannel
GetAccessDataByAuthCode(authCode string) StoreChannel
@@ -161,12 +169,14 @@ type WebhookStore interface {
GetIncomingByUser(userId string) StoreChannel
GetIncomingByChannel(channelId string) StoreChannel
DeleteIncoming(webhookId string, time int64) StoreChannel
+ PermanentDeleteIncomingByUser(userId string) StoreChannel
SaveOutgoing(webhook *model.OutgoingWebhook) StoreChannel
GetOutgoing(id string) StoreChannel
GetOutgoingByCreator(userId string) StoreChannel
GetOutgoingByChannel(channelId string) StoreChannel
GetOutgoingByTeam(teamId string) StoreChannel
DeleteOutgoing(webhookId string, time int64) StoreChannel
+ PermanentDeleteOutgoingByUser(userId string) StoreChannel
UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel
}
@@ -175,4 +185,5 @@ type PreferenceStore interface {
Get(userId string, category string, name string) StoreChannel
GetCategory(userId string, category string) StoreChannel
GetAll(userId string) StoreChannel
+ PermanentDeleteByUser(userId string) StoreChannel
}
diff --git a/utils/config.go b/utils/config.go
index 13b7b6b64..2fd799cd1 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -59,8 +59,7 @@ func FindDir(dir string) string {
func ConfigureCmdLineLog() {
ls := model.LogSettings{}
ls.EnableConsole = true
- ls.ConsoleLevel = "ERROR"
- ls.EnableFile = false
+ ls.ConsoleLevel = "WARN"
configureLog(&ls)
}
@@ -72,6 +71,8 @@ func configureLog(s *model.LogSettings) {
level := l4g.DEBUG
if s.ConsoleLevel == "INFO" {
level = l4g.INFO
+ } else if s.ConsoleLevel == "WARN" {
+ level = l4g.WARNING
} else if s.ConsoleLevel == "ERROR" {
level = l4g.ERROR
}
@@ -90,6 +91,8 @@ func configureLog(s *model.LogSettings) {
level := l4g.DEBUG
if s.FileLevel == "INFO" {
level = l4g.INFO
+ } else if s.FileLevel == "WARN" {
+ level = l4g.WARNING
} else if s.FileLevel == "ERROR" {
level = l4g.ERROR
}
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index ffe7cbb5d..e46b2ccd7 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -27,6 +27,7 @@ const ActionTypes = Constants.ActionTypes;
const Popover = ReactBootstrap.Popover;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+const Tooltip = ReactBootstrap.Tooltip;
export default class ChannelHeader extends React.Component {
constructor(props) {
@@ -121,6 +122,7 @@ export default class ChannelHeader extends React.Component {
}
const channel = this.state.channel;
+ const recentMentionsTooltip = <Tooltip id='recentMentionsTooltip'>{'Recent Mentions'}</Tooltip>;
const popoverContent = (
<Popover
id='hader-popover'
@@ -382,31 +384,19 @@ export default class ChannelHeader extends React.Component {
<th className='search-bar__container'><NavbarSearchBox /></th>
<th>
<div className='dropdown channel-header__links'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- id='channel_header_right_dropdown'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
- </a>
- <ul
- className='dropdown-menu dropdown-menu-right'
- role='menu'
- aria-labelledby='channel_header_right_dropdown'
+ <OverlayTrigger
+ delayShow={400}
+ placement='bottom'
+ overlay={recentMentionsTooltip}
>
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={this.searchMentions}
- >
- {'Recent Mentions'}
- </a>
- </li>
- </ul>
+ <a
+ href='#'
+ type='button'
+ onClick={this.searchMentions}
+ >
+ {'@'}
+ </a>
+ </OverlayTrigger>
</div>
</th>
</tr>
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 058594165..22a659ed5 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -194,7 +194,8 @@ export default class CreateComment extends React.Component {
title: 'Comment',
message: lastPost.message,
postId: lastPost.id,
- channelId: lastPost.channel_id
+ channelId: lastPost.channel_id,
+ comments: PostStore.getCommentCount(lastPost)
});
}
}
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 5a69c9bfb..d823a54f1 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -372,7 +372,8 @@ export default class CreatePost extends React.Component {
title: type,
message: lastPost.message,
postId: lastPost.id,
- channelId: lastPost.channel_id
+ channelId: lastPost.channel_id,
+ comments: PostStore.getCommentCount(lastPost)
});
}
}
@@ -382,8 +383,8 @@ export default class CreatePost extends React.Component {
screens.push(
<div>
<h4>{'Sending Messages'}</h4>
- <p>{'Type here to write a message.'}</p>
- <p>{'Click the attachment button to upload an image or a file.'}</p>
+ <p>{'Type here to write a message and press '}<strong>{'Enter'}</strong>{' to post it.'}</p>
+ <p>{'Click the '}<strong>{'Attachment'}</strong>{' button to upload an image or a file.'}</p>
</div>
);
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index f3bead1c2..e0489856f 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -3,7 +3,8 @@
var Client = require('../utils/client.jsx');
var PostStore = require('../stores/post_store.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
+var ModalStore = require('../stores/modal_store.jsx');
+var Modal = ReactBootstrap.Modal;
var Utils = require('../utils/utils.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
@@ -15,18 +16,40 @@ export default class DeletePostModal extends React.Component {
super(props);
this.handleDelete = this.handleDelete.bind(this);
+ this.handleToggle = this.handleToggle.bind(this);
+ this.handleHide = this.handleHide.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
- this.onShow = this.onShow.bind(this);
- this.state = {title: '', postId: '', channelId: '', selectedList: PostStore.getSelectedPost(), comments: 0};
+ this.selectedList = null;
+
+ this.state = {
+ show: true,
+ post: null,
+ commentCount: 0,
+ error: ''
+ };
+ }
+
+ componentDidMount() {
+ ModalStore.addModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
+ PostStore.addSelectedPostChangeListener(this.onListenerChange);
+ }
+
+ componentWillUnmount() {
+ PostStore.removeSelectedPostChangeListener(this.onListenerChange);
+ ModalStore.removeModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
}
+
handleDelete() {
- Client.deletePost(this.state.channelId, this.state.postId,
- function deleteSuccess() {
- var selectedList = this.state.selectedList;
+ Client.deletePost(
+ this.state.post.channel_id,
+ this.state.post.id,
+ () => {
+ var selectedList = this.selectedList;
+
if (selectedList && selectedList.order && selectedList.order.length > 0) {
var selectedPost = selectedList.posts[selectedList.order[0]];
- if ((selectedPost.id === this.state.postId && this.state.title === 'Post') || selectedPost.root_id === this.state.postId) {
+ if ((selectedPost.id === this.state.post.id && !this.state.root_id) || selectedPost.root_id === this.state.post.id) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_SEARCH,
results: null
@@ -36,7 +59,7 @@ export default class DeletePostModal extends React.Component {
type: ActionTypes.RECIEVED_POST_SELECTED,
results: null
});
- } else if (selectedPost.id === this.state.postId && this.state.title === 'Comment') {
+ } else if (selectedPost.id === this.state.post.id && this.state.root_id) {
if (selectedPost.root_id && selectedPost.root_id.length > 0 && selectedList.posts[selectedPost.root_id]) {
selectedList.order = [selectedPost.root_id];
delete selectedList.posts[selectedPost.id];
@@ -53,98 +76,96 @@ export default class DeletePostModal extends React.Component {
}
}
}
- PostStore.removePost(this.state.postId, this.state.channelId);
- AsyncClient.getPosts(this.state.channelId);
- }.bind(this),
- function deleteFailed(err) {
+
+ PostStore.removePost(this.state.post.id, this.state.post.channel_id);
+ AsyncClient.getPosts(this.state.post.channel_id);
+ },
+ (err) => {
AsyncClient.dispatchError(err, 'deletePost');
}
);
+
+ this.handleHide();
}
- onShow(e) {
- var newState = {};
- if (BrowserStore.getItem('edit_state_transfer')) {
- newState = BrowserStore.getItem('edit_state_transfer');
- BrowserStore.removeItem('edit_state_transfer');
- } else {
- var button = e.relatedTarget;
- newState = {title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), postId: $(button).attr('data-postid'), comments: $(button).attr('data-comments')};
- }
- this.setState(newState);
- }
- componentDidMount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- PostStore.addSelectedPostChangeListener(this.onListenerChange);
+
+ handleToggle(value, args) {
+ this.setState({
+ show: value,
+ post: args.post,
+ commentCount: args.commentCount,
+ error: ''
+ });
}
- componentWillUnmount() {
- PostStore.removeSelectedPostChangeListener(this.onListenerChange);
+
+ handleHide() {
+ this.setState({show: false});
}
+
onListenerChange() {
var newList = PostStore.getSelectedPost();
- if (!Utils.areObjectsEqual(this.state.selectedList, newList)) {
- this.setState({selectedList: newList});
+ if (!Utils.areObjectsEqual(this.selectedList, newList)) {
+ this.selectedList = newList;
}
}
+
render() {
+ if (!this.state.post) {
+ return null;
+ }
+
var error = null;
if (this.state.error) {
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
}
var commentWarning = '';
- if (this.state.comments > 0) {
- commentWarning = 'This post has ' + this.state.comments + ' comment(s) on it.';
+ if (this.state.commentCount > 0) {
+ commentWarning = 'This post has ' + this.state.commentCount + ' comment(s) on it.';
}
+ const postTerm = Utils.getPostTerm(this.state.post);
+
return (
- <div
- className='modal fade'
- id='delete_post'
- ref='modal'
- role='dialog'
- tabIndex='-1'
- aria-hidden='true'
+ <Modal
+ show={this.state.show}
+ onHide={this.handleHide}
>
- <div className='modal-dialog modal-push-down'>
- <div className='modal-content'>
- <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'>Confirm {this.state.title} Delete</h4>
- </div>
- <div className='modal-body'>
- Are you sure you want to delete the {this.state.title.toLowerCase()}?
- <br/>
- <br/>
+ <Modal.Header closeButton={true}>
+ {`Confirm ${postTerm} Delete`}
+ </Modal.Header>
+ <Modal.Body>
+ {`Are you sure you want to delete this ${postTerm.toLowerCase()}?`}
+ <br />
+ <br />
{commentWarning}
- </div>
- {error}
- <div className='modal-footer'>
+ {error}
+ </Modal.Body>
+ <Modal.Footer>
<button
type='button'
className='btn btn-default'
- data-dismiss='modal'
+ onClick={this.handleHide}
>
- Cancel
+ {'Cancel'}
</button>
<button
type='button'
className='btn btn-danger'
- data-dismiss='modal'
onClick={this.handleDelete}
>
- Delete
+ {'Delete'}
</button>
- </div>
- </div>
- </div>
- </div>
+ </Modal.Footer>
+ </Modal>
);
}
+
+ static show(post, commentCount) {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
+ value: true,
+ post,
+ commentCount: commentCount || 0
+ });
+ }
}
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index ef32baa7d..c75da75c9 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -3,6 +3,7 @@
var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
+var DeletePostModal = require('./delete_post_modal.jsx');
var Textbox = require('./textbox.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
var PostStore = require('../stores/post_store.jsx');
@@ -34,7 +35,7 @@ export default class EditPostModal extends React.Component {
delete tempState.editText;
BrowserStore.setItem('edit_state_transfer', tempState);
$('#edit_post').modal('hide');
- $('#delete_post').modal('show');
+ DeletePostModal.show(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
return;
}
diff --git a/web/react/components/file_preview.jsx b/web/react/components/file_preview.jsx
index df5deb8bc..b8c86ed67 100644
--- a/web/react/components/file_preview.jsx
+++ b/web/react/components/file_preview.jsx
@@ -8,9 +8,14 @@ export default class FilePreview extends React.Component {
super(props);
this.handleRemove = this.handleRemove.bind(this);
+ }
- this.state = {};
+ componentDidUpdate() {
+ if (this.props.uploadsInProgress.length > 0) {
+ ReactDOM.findDOMNode(this.refs[this.props.uploadsInProgress[0]]).scrollIntoView();
+ }
}
+
handleRemove(e) {
var previewDiv = e.target.parentNode.parentNode;
@@ -20,9 +25,10 @@ export default class FilePreview extends React.Component {
this.props.onRemove(previewDiv.getAttribute('data-client-id'));
}
}
+
render() {
var previews = [];
- this.props.files.forEach(function setupPreview(fullFilename) {
+ this.props.files.forEach((fullFilename) => {
var filename = fullFilename;
var originalFilename = filename;
var filenameSplit = filename.split('.');
@@ -72,11 +78,12 @@ export default class FilePreview extends React.Component {
</div>
);
}
- }.bind(this));
+ });
- this.props.uploadsInProgress.forEach(function addUploadsInProgress(clientId) {
+ this.props.uploadsInProgress.forEach((clientId) => {
previews.push(
<div
+ ref={clientId}
key={clientId}
className='preview-div'
data-client-id={clientId}
@@ -93,7 +100,7 @@ export default class FilePreview extends React.Component {
</a>
</div>
);
- }.bind(this));
+ });
return (
<div className='preview-container'>
@@ -104,8 +111,8 @@ export default class FilePreview extends React.Component {
}
FilePreview.defaultProps = {
- files: null,
- uploadsInProgress: null
+ files: [],
+ uploadsInProgress: []
};
FilePreview.propTypes = {
onRemove: React.PropTypes.func.isRequired,
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 2b9586345..7e4af07c4 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -13,8 +13,6 @@ var AsyncClient = require('../utils/async_client.jsx');
var ActionTypes = Constants.ActionTypes;
var utils = require('../utils/utils.jsx');
-var PostInfo = require('./post_info.jsx');
-
export default class Post extends React.Component {
constructor(props) {
super(props);
@@ -141,6 +139,8 @@ export default class Post extends React.Component {
var postType = '';
if (type !== 'Post') {
postType = 'post--comment';
+ } else if (commentCount > 0) {
+ postType = 'post--root';
}
var currentUserCss = '';
@@ -170,14 +170,11 @@ export default class Post extends React.Component {
}
profilePic = (
- <div className='post-profile-img__container'>
- <img
- className='post-profile-img'
- src={src}
- height='36'
- width='36'
- />
- </div>
+ <img
+ src={src}
+ height='36'
+ width='36'
+ />
);
}
@@ -187,32 +184,26 @@ export default class Post extends React.Component {
id={'post_' + post.id}
className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}
>
- {profilePic}
<div className='post__content'>
- <PostHeader
- ref='header'
- post={post}
- sameRoot={this.props.sameRoot}
- commentCount={commentCount}
- handleCommentClick={this.handleCommentClick}
- isLastComment={this.props.isLastComment}
- />
- <PostBody
- post={post}
- sameRoot={this.props.sameRoot}
- parentPost={parentPost}
- posts={posts}
- handleCommentClick={this.handleCommentClick}
- retryPost={this.retryPost}
- />
- <PostInfo
- ref='info'
- post={post}
- sameRoot={this.props.sameRoot}
- commentCount={commentCount}
- handleCommentClick={this.handleCommentClick}
- allowReply='true'
- />
+ <div className='post__img'>{profilePic}</div>
+ <div>
+ <PostHeader
+ ref='header'
+ post={post}
+ sameRoot={this.props.sameRoot}
+ commentCount={commentCount}
+ handleCommentClick={this.handleCommentClick}
+ isLastComment={this.props.isLastComment}
+ />
+ <PostBody
+ post={post}
+ sameRoot={this.props.sameRoot}
+ parentPost={parentPost}
+ posts={posts}
+ handleCommentClick={this.handleCommentClick}
+ retryPost={this.retryPost}
+ />
+ </div>
</div>
</div>
</div>
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 975ac64dc..e1c057775 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -257,7 +257,7 @@ export default class PostBody extends React.Component {
}
return (
- <div className='post-comment'>
+ <div>
<h4>
<span className='video-type'>{header}</span>
<span className='video-title'><a href={link}>{this.state.youtubeTitle}</a></span>
@@ -329,7 +329,7 @@ export default class PostBody extends React.Component {
}
comment = (
- <p className='post-link'>
+ <div className='post__link'>
<span>
{'Commented on '}{name}{apostrophe}{' message: '}
<a
@@ -339,15 +339,13 @@ export default class PostBody extends React.Component {
{message}
</a>
</span>
- </p>
+ </div>
);
-
- postClass += ' post-comment';
}
let loading;
if (post.state === Constants.POST_FAILED) {
- postClass += ' post-fail';
+ postClass += ' post--fail';
loading = (
<a
className='theme post-retry pull-right'
@@ -379,25 +377,27 @@ export default class PostBody extends React.Component {
}
return (
- <div className='post-body'>
+ <div>
{comment}
- <div
- key={`${post.id}_message`}
- id={`${post.id}_message`}
- className={postClass}
- >
- {loading}
- <span
- ref='message_span'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
+ <div className='post__body'>
+ <div
+ key={`${post.id}_message`}
+ id={`${post.id}_message`}
+ className={postClass}
+ >
+ {loading}
+ <span
+ ref='message_span'
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
+ />
+ </div>
+ <PostBodyAdditionalContent
+ post={this.state.post}
/>
+ {fileAttachmentHolder}
+ {this.embed}
</div>
- <PostBodyAdditionalContent
- post={this.state.post}
- />
- {fileAttachmentHolder}
- {this.embed}
</div>
);
}
diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx
index 45e60c767..e8b162fc2 100644
--- a/web/react/components/post_header.jsx
+++ b/web/react/components/post_header.jsx
@@ -26,14 +26,14 @@ export default class PostHeader extends React.Component {
);
}
- botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>;
+ botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
}
return (
- <ul className='post-header post-header-post'>
- <li className='post-header-col post-header__name'><strong>{userProfile}</strong></li>
+ <ul className='post__header'>
+ <li className='col col__name'>{userProfile}</li>
{botIndicator}
- <li className='post-info--hidden'>
+ <li className='col'>
<PostInfo
post={post}
commentCount={this.props.commentCount}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index a01d842e5..5446fca7a 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+var DeletePostModal = require('./delete_post_modal.jsx');
var UserStore = require('../stores/user_store.jsx');
var utils = require('../utils/utils.jsx');
var TimeSince = require('./time_since.jsx');
@@ -50,7 +51,7 @@ export default class PostInfo extends React.Component {
data-channelid={post.channel_id}
data-comments={dataComments}
>
- Edit
+ {'Edit'}
</a>
</li>
);
@@ -65,31 +66,9 @@ export default class PostInfo extends React.Component {
<a
href='#'
role='menuitem'
- data-toggle='modal'
- data-target='#delete_post'
- data-title={type}
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={dataComments}
- >
- Delete
- </a>
- </li>
- );
- }
-
- if (this.props.allowReply === 'true') {
- dropdownContents.push(
- <li
- key='replyLink'
- role='presentation'
- >
- <a
- className='reply-link theme'
- href='#'
- onClick={this.props.handleCommentClick}
+ onClick={() => DeletePostModal.show(post, dataComments)}
>
- Reply
+ {'Delete'}
</a>
</li>
);
@@ -103,7 +82,7 @@ export default class PostInfo extends React.Component {
<div>
<a
href='#'
- className='dropdown-toggle theme'
+ className='dropdown-toggle post__dropdown theme'
type='button'
data-toggle='dropdown'
aria-expanded='false'
@@ -120,23 +99,27 @@ export default class PostInfo extends React.Component {
render() {
var post = this.props.post;
var comments = '';
- var lastCommentClass = ' comment-icon__container__hide';
- if (this.props.isLastComment) {
- lastCommentClass = ' comment-icon__container__show';
+ var showCommentClass = '';
+ var commentCountText = this.props.commentCount;
+
+ if (this.props.commentCount >= 1) {
+ showCommentClass = ' icon--show';
+ } else {
+ commentCountText = '';
}
- if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) {
+ if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) {
comments = (
<a
href='#'
- className={'comment-icon__container theme' + lastCommentClass}
+ className={'comment-icon__container' + showCommentClass}
onClick={this.props.handleCommentClick}
>
<span
className='comment-icon'
dangerouslySetInnerHTML={{__html: Constants.COMMENT_ICON}}
/>
- {this.props.commentCount}
+ {commentCountText}
</a>
);
}
@@ -144,17 +127,17 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
return (
- <ul className='post-header post-info'>
- <li className='post-header-col'>
+ <ul className='post__header post__header--info'>
+ <li className='col'>
<TimeSince
eventTime={post.create_at}
/>
</li>
- <li className='post-header-col post-header__reply'>
+ <li className='col col__reply'>
+ {comments}
<div className='dropdown'>
{dropdown}
</div>
- {comments}
</li>
</ul>
);
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 58cc1cac7..a55bf0039 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -8,6 +8,7 @@ var UserStore = require('../stores/user_store.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
+var DeletePostModal = require('./delete_post_modal.jsx');
var FileAttachmentList = require('./file_attachment_list.jsx');
var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
@@ -114,12 +115,7 @@ export default class RhsComment extends React.Component {
<a
href='#'
role='menuitem'
- data-toggle='modal'
- data-target='#delete_post'
- data-title='Comment'
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={0}
+ onClick={() => DeletePostModal.show(post, 0)}
>
{'Delete'}
</a>
@@ -135,7 +131,7 @@ export default class RhsComment extends React.Component {
<div className='dropdown'>
<a
href='#'
- className='dropdown-toggle theme'
+ className='post__dropdown dropdown-toggle'
type='button'
data-toggle='dropdown'
aria-expanded='false'
@@ -197,38 +193,39 @@ export default class RhsComment extends React.Component {
return (
<div className={'post ' + currentUserCss}>
- <div className='post-profile-img__container'>
- <img
- className='post-profile-img'
- src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
- height='36'
- width='36'
- />
- </div>
<div className='post__content'>
- <ul className='post-header'>
- <li className='post-header-col'>
- <strong><UserProfile userId={post.user_id} /></strong>
- </li>
- <li className='post-header-col'>
- <time className='post-profile-time'>
- {Utils.displayCommentDateTime(post.create_at)}
- </time>
- </li>
- <li className='post-header-col post-header__reply'>
- {dropdown}
- </li>
- </ul>
- <div className='post-body'>
- <div className={postClass}>
- {loading}
- <div
- ref='message_holder'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
- />
+ <div className='post__img'>
+ <img
+ src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
+ height='36'
+ width='36'
+ />
+ </div>
+ <div>
+ <ul className='post__header'>
+ <li className='col__name'>
+ <strong><UserProfile userId={post.user_id} /></strong>
+ </li>
+ <li className='col'>
+ <time className='post__time'>
+ {Utils.displayCommentDateTime(post.create_at)}
+ </time>
+ </li>
+ <li className='col col__reply'>
+ {dropdown}
+ </li>
+ </ul>
+ <div className='post__body'>
+ <div className={postClass}>
+ {loading}
+ <div
+ ref='message_holder'
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
+ />
+ </div>
+ {fileAttachment}
</div>
- {fileAttachment}
</div>
</div>
</div>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 69de5d523..358bf8440 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -6,6 +6,7 @@ var UserProfile = require('./user_profile.jsx');
var UserStore = require('../stores/user_store.jsx');
var TextFormatting = require('../utils/text_formatting.jsx');
var utils = require('../utils/utils.jsx');
+var DeletePostModal = require('./delete_post_modal.jsx');
var FileAttachmentList = require('./file_attachment_list.jsx');
var twemoji = require('twemoji');
var Constants = require('../utils/constants.jsx');
@@ -65,7 +66,7 @@ export default class RhsRootPost extends React.Component {
ownerOptions = (
<div>
<a href='#'
- className='dropdown-toggle theme'
+ className='post__dropdown dropdown-toggle'
type='button'
data-toggle='dropdown'
aria-expanded='false'
@@ -86,21 +87,16 @@ export default class RhsRootPost extends React.Component {
data-postid={post.id}
data-channelid={post.channel_id}
>
- Edit
+ {'Edit'}
</a>
</li>
<li role='presentation'>
<a
href='#'
role='menuitem'
- data-toggle='modal'
- data-target='#delete_post'
- data-title={type}
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={this.props.commentCount}
+ onClick={() => DeletePostModal.show(post, this.props.commentCount)}
>
- Delete
+ {'Delete'}
</a>
</li>
</ul>
@@ -133,7 +129,7 @@ export default class RhsRootPost extends React.Component {
);
}
- botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>;
+ botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
}
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
@@ -144,47 +140,47 @@ export default class RhsRootPost extends React.Component {
}
const profilePic = (
- <div className='post-profile-img__container'>
- <img
- className='post-profile-img'
- src={src}
- height='36'
- width='36'
- />
- </div>
+ <img
+ className='post-profile-img'
+ src={src}
+ height='36'
+ width='36'
+ />
);
return (
<div className={'post post--root ' + currentUserCss}>
<div className='post-right-channel__name'>{channelName}</div>
- <div className='post-profile-img__container'>
- {profilePic}
- </div>
<div className='post__content'>
- <ul className='post-header'>
- <li className='post-header-col'><strong>{userProfile}</strong></li>
- {botIndicator}
- <li className='post-header-col'>
- <time className='post-profile-time'>
- {utils.displayCommentDateTime(post.create_at)}
- </time>
- </li>
- <li className='post-header-col post-header__reply'>
- <div className='dropdown'>
- {ownerOptions}
- </div>
- </li>
- </ul>
- <div className='post-body'>
- <div
- ref='message_holder'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
- />
- <PostBodyAdditionalContent
- post={post}
- />
- {fileAttachment}
+ <div className='post__img'>
+ {profilePic}
+ </div>
+ <div>
+ <ul className='post__header'>
+ <li className='col__name'>{userProfile}</li>
+ {botIndicator}
+ <li className='col'>
+ <time className='post__time'>
+ {utils.displayCommentDateTime(post.create_at)}
+ </time>
+ </li>
+ <li className='col col__reply'>
+ <div className='dropdown'>
+ {ownerOptions}
+ </div>
+ </li>
+ </ul>
+ <div className='post__body'>
+ <div
+ ref='message_holder'
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
+ />
+ <PostBodyAdditionalContent
+ post={post}
+ />
+ {fileAttachment}
+ </div>
</div>
</div>
<hr />
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index a8bd4db2c..52766a8a0 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -74,29 +74,30 @@ export default class SearchResultsItem extends React.Component {
onClick={this.handleClick}
>
<div className='search-channel__name'>{channelName}</div>
- <div className='post-profile-img__container'>
- <img
- className='post-profile-img'
- src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()}
- height='36'
- width='36'
- />
- </div>
<div className='post__content'>
- <ul className='post-header'>
- <li className='post-header-col'><strong><UserProfile userId={this.props.post.user_id} /></strong></li>
- <li className='post-header-col'>
- <time className='search-item-time'>
- {utils.displayDate(this.props.post.create_at) + ' ' + utils.displayTime(this.props.post.create_at)}
- </time>
- </li>
- </ul>
- <div className='search-item-snippet'>
- <span
- onClick={this.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}}
+ <div className='post__img'>
+ <img
+ src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()}
+ height='36'
+ width='36'
/>
</div>
+ <div>
+ <ul className='post__header'>
+ <li className='col__name'><strong><UserProfile userId={this.props.post.user_id} /></strong></li>
+ <li className='col'>
+ <time className='search-item-time'>
+ {utils.displayDate(this.props.post.create_at) + ' ' + utils.displayTime(this.props.post.create_at)}
+ </time>
+ </li>
+ </ul>
+ <div className='search-item-snippet'>
+ <span
+ onClick={this.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}}
+ />
+ </div>
+ </div>
</div>
</div>
);
diff --git a/web/react/components/time_since.jsx b/web/react/components/time_since.jsx
index c37739b9c..212beb080 100644
--- a/web/react/components/time_since.jsx
+++ b/web/react/components/time_since.jsx
@@ -34,7 +34,7 @@ export default class TimeSince extends React.Component {
placement='top'
overlay={tooltip}
>
- <time className='post-profile-time'>
+ <time className='post__time'>
{Utils.displayDateTime(this.props.eventTime)}
</time>
</OverlayTrigger>
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index 3dbed72c3..3d0a2b548 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -14,7 +14,10 @@ export default class CustomThemeChooser extends React.Component {
this.state = {};
}
componentDidMount() {
- $('.color-picker').colorpicker().on('changeColor', this.onPickerChange);
+ $('.color-picker').colorpicker({
+ format: 'hex'
+ });
+ $('.color-picker').on('changeColor', this.onPickerChange);
}
onPickerChange(e) {
const theme = this.props.theme;
diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx
index dc65d48da..809f83a59 100644
--- a/web/react/stores/modal_store.jsx
+++ b/web/react/stores/modal_store.jsx
@@ -27,12 +27,14 @@ class ModalStoreClass extends EventEmitter {
}
handleEventPayload(payload) {
- const action = payload.action;
+ // toggle event handlers should accept a boolean show/hide value and can accept a map of arguments
+ const {type, value, ...args} = payload.action;
- switch (action.type) {
+ switch (type) {
case ActionTypes.TOGGLE_IMPORT_THEME_MODAL:
case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL:
- this.emit(action.type, action.value);
+ case ActionTypes.TOGGLE_DELETE_POST_MODAL:
+ this.emit(type, value, args);
break;
}
}
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 0fe253310..a564a2435 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -40,6 +40,7 @@ class PostStoreClass extends EventEmitter {
this.storePosts = this.storePosts.bind(this);
this.pStorePosts = this.pStorePosts.bind(this);
this.getPosts = this.getPosts.bind(this);
+ this.getPost = this.getPost.bind(this);
this.storePost = this.storePost.bind(this);
this.pStorePost = this.pStorePost.bind(this);
this.removePost = this.removePost.bind(this);
@@ -68,6 +69,7 @@ class PostStoreClass extends EventEmitter {
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
this.getLatestUpdate = this.getLatestUpdate.bind(this);
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
+ this.getCommentCount = this.getCommentCount.bind(this);
}
emitChange() {
this.emit(CHANGE_EVENT);
@@ -193,6 +195,9 @@ class PostStoreClass extends EventEmitter {
getPosts(channelId) {
return BrowserStore.getItem('posts_' + channelId);
}
+ getPost(channelId, postId) {
+ return this.getPosts(channelId).posts[postId];
+ }
getCurrentUsersLatestPost(channelId, rootId) {
const userId = UserStore.getCurrentId();
var postList = makePostListNonNull(this.getPosts(channelId));
@@ -402,6 +407,20 @@ class PostStoreClass extends EventEmitter {
getLatestUpdate(channelId) {
return BrowserStore.getItem('latest_post_' + channelId, 0);
}
+ getCommentCount(post) {
+ const posts = this.getPosts(post.channel_id).posts;
+
+ let commentCount = 0;
+ for (let id in posts) {
+ if (posts.hasOwnProperty(id)) {
+ if (posts[id].root_id === post.id) {
+ commentCount += 1;
+ }
+ }
+ }
+
+ return commentCount;
+ }
}
var PostStore = new PostStoreClass();
diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_mssages.jsx
index 11b69e28f..28a458e3f 100644
--- a/web/react/utils/channel_intro_mssages.jsx
+++ b/web/react/utils/channel_intro_mssages.jsx
@@ -205,6 +205,7 @@ export function createStandardIntroMessage(channel, showInviteModal) {
<i className='fa fa-pencil'></i>{'Set a header'}
</a>
<a
+ className='intro-links'
href='#'
onClick={showInviteModal}
>
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 4fef64f18..80c0cf0ee 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -42,7 +42,8 @@ module.exports = {
SHOW_SEARCH: null,
TOGGLE_IMPORT_THEME_MODAL: null,
- TOGGLE_INVITE_MEMBER_MODAL: null
+ TOGGLE_INVITE_MEMBER_MODAL: null,
+ TOGGLE_DELETE_POST_MODAL: null
}),
PayloadSources: keyMirror({
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 77b3ecb57..bf40d8c65 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -538,12 +538,11 @@ export function applyTheme(theme) {
if (theme.sidebarText) {
changeCss('.sidebar--left .nav-pills__container li>a, .sidebar--right, .settings-modal .nav-pills>li a, .sidebar--menu', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1);
- changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
+ changeCss('@media(max-width: 960px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1);
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
- changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1);
changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
- changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
+ changeCss('@media(max-width: 960px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
}
if (theme.sidebarUnreadText) {
@@ -552,7 +551,7 @@ export function applyTheme(theme) {
if (theme.sidebarTextHoverBg) {
changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'background:' + theme.sidebarTextHoverBg, 1);
- changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('@media(max-width: 960px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
}
if (theme.sidebarTextActiveBorder) {
@@ -570,7 +569,7 @@ export function applyTheme(theme) {
changeCss('.sidebar--left .team__header, .sidebar--menu .team__header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
- changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('@media(max-width: 960px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('.attachment .attachment__container', 'border-left-color:' + theme.sidebarHeaderBg, 1);
}
@@ -581,7 +580,7 @@ export function applyTheme(theme) {
changeCss('.modal .modal-header .modal-title, .modal .modal-header .modal-title .name, .modal .modal-header button.close', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('#navbar .navbar-default .navbar-brand .heading', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('#navbar .navbar-default .navbar-toggle .icon-bar, ', 'background:' + theme.sidebarHeaderTextColor, 1);
- changeCss('@media(max-width: 768px){.search-bar__container', 'color:' + theme.sidebarHeaderTextColor, 2);
+ changeCss('@media(max-width: 960px){.search-bar__container', 'color:' + theme.sidebarHeaderTextColor, 2);
}
if (theme.onlineIndicator) {
@@ -614,17 +613,16 @@ export function applyTheme(theme) {
}
if (theme.centerChannelColor) {
+ changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name, .tip-overlay', 'color:' + theme.centerChannelColor, 1);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
- changeCss('.channel-header__links a', 'fill:' + changeOpacity(theme.centerChannelColor, 0.9), 1);
- changeCss('.channel-header__links a:hover, .channel-header__links a:active', 'fill:' + theme.centerChannelColor, 2);
changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
changeCss('.mentions--top, .command-box', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1);
changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3);
changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2);
changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1);
- changeCss('.post-body hr, .loading-screen .loading__content .round, .tutorial__circles .circle', 'background:' + theme.centerChannelColor, 1);
+ changeCss('.post__body hr, .loading-screen .loading__content .round, .tutorial__circles .circle', 'background:' + theme.centerChannelColor, 1);
changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1);
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
@@ -642,25 +640,23 @@ export function applyTheme(theme) {
changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
changeCss('.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
- changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
+ changeCss('@media(max-width: 960px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
- changeCss('.form-control:focus, .channel-header__links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
- changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, pre', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2);
- changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
- changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('.post.current--user .post__body, .post.post--comment.other--root.current--user .post-comment, pre', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post.current--user .post__body, .post.post--comment.other--root.current--user .post-comment, .post.same--root.post--comment .post__body, .modal .more-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2);
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
- changeCss('.post:hover, .channel-header__links a:hover, .channel-header__links a:active, .modal .more-table tbody>tr:hover td, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
- changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
+ changeCss('.post.current--user:hover .post__body ', 'background: none;', 1);
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
}
@@ -1232,3 +1228,12 @@ export function getChannelTerm(channelType) {
return channelTerm;
}
+
+export function getPostTerm(post) {
+ let postTerm = 'Post';
+ if (post.root_id) {
+ postTerm = 'Comment';
+ }
+
+ return postTerm;
+}
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index ad4a65c00..c5b1a7425 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -85,6 +85,7 @@ img {
a {
word-break: break-word;
color: $primary-color;
+ cursor: pointer;
}
a:focus, a:hover {
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index 69bc56841..51625afe0 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -54,9 +54,9 @@
}
.channel-intro {
- padding-bottom:5px;
- margin: 0 1em 35px;
- max-width: 850px;
+ margin: 0 auto 35px;
+ padding: 0 1em 5px;
+ max-width: 1000px;
border-bottom: 1px solid lightgrey;
.intro-links {
margin: 0 1.5em 10px 0;
@@ -216,7 +216,6 @@
font-size: 14px;
line-height: 56px;
#member_popover {
- margin-right: 5px;
width: 45px;
color: #999;
cursor: pointer;
@@ -292,20 +291,21 @@
}
.channel-header__links {
- height: 32px;
- width: 32px;
- @include border-radius(50px);
- border: 1px solid #ccc;
+ height: 30px;
+ width: 24px;
+ line-height: 26px;
margin-right: 10px;
+ font-size: 22px;
> a {
- @include border-radius(50px);
- height: 100%;
- display: block;
+ color: inherit;
+ text-decoration: none;
+ @include opacity(0.6);
@include single-transition(all, 0.1s, ease-in);
- }
- svg {
- vertical-align: top;
- margin: 7px 0 0 0px;
- fill: inherit;
+ &:hover {
+ @include opacity(1);
+ }
+ &:focus {
+ color: inherit;
+ }
}
}
diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss
index 8b0a32704..ea5ccd2d2 100644
--- a/web/sass-files/sass/partials/_markdown.scss
+++ b/web/sass-files/sass/partials/_markdown.scss
@@ -16,7 +16,7 @@
}
}
-.post-body {
+.post__body {
hr {
height: 4px;
padding: 0;
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index fad6f5074..743a76693 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -47,23 +47,23 @@ body.ios {
.textarea-wrapper {
position:relative;
- .textbox-preview-area {
- position: absolute;
- z-index: 2;
- top: 0;
- left: 0;
- box-shadow: none;
- }
+ .textbox-preview-area {
+ position: absolute;
+ z-index: 2;
+ top: 0;
+ left: 0;
+ box-shadow: none;
+ }
.textbox-preview-link, .textbox-help-link {
- position: absolute;
- z-index: 3;
- bottom: -23px;
+ position: absolute;
+ z-index: 3;
+ bottom: -23px;
font-size: 13px;
- cursor: pointer;
- }
- .textbox-preview-link {
- right: 45px;
- }
+ cursor: pointer;
+ }
+ .textbox-preview-link {
+ right: 45px;
+ }
.textbox-help-link {
right: 0;
}
@@ -206,6 +206,7 @@ body.ios {
@include flex(1 1 auto);
position: relative;
overflow-y: hidden;
+
.post-list-holder-by-time {
background: #fff;
overflow-y: scroll;
@@ -224,31 +225,7 @@ body.ios {
display: inline;
}
}
- .post-list__table {
- display: table;
- table-layout: fixed;
- width: 100%;
- min-height: 100%;
- height: 100%;
- .post-list__content {
- display: table-cell;
- vertical-align: bottom;
- div {
- &:last-child {
- .post {
- .post-header {
- .post-header-col.post-header__reply {
- .dropdown-menu {
- top: auto;
- bottom: 25px;
- }
- }
- }
- }
- }
- }
- }
- }
+
.more-messages-text {
margin-top: 2px;
margin-bottom: 5px;
@@ -269,13 +246,13 @@ body.ios {
.post-create__container {
form {
width: 100%;
- padding: 0 1em;
- margin: 0;
+ padding: 0.5em 14px 0;
+ margin: 0 auto;
+ max-width: 1028px;
}
.post-create-body {
- max-width: 850px;
- padding: 0 0 2px;
position: relative;
+ padding: 0 0 2px;
.post-body__cell {
vertical-align: top;
position: relative;
@@ -355,95 +332,271 @@ body.ios {
}
}
+.post-list__table {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+ min-height: 100%;
+ height: 100%;
+ .post-list__content {
+ display: table-cell;
+ vertical-align: bottom;
+ > div:last-child {
+ .dropdown-menu {
+ top: auto;
+ bottom: 25px;
+ }
+ }
+ }
+}
+
.post {
word-wrap: break-word;
- padding: 8px 1em;
+ padding: 8px 1em 0;
position: relative;
max-width: 100%;
@include legacy-pie-clearfix;
+
&:hover {
- .post-header .post-header-col.post-header__reply {
- .dropdown, .comment-icon__container {
- @include opacity(1);
- }
+ .dropdown, .comment-icon__container {
+ visibility: visible;
+ }
+ }
+
+ ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ p {
+ margin: 0 0 1em;
+ line-height: 1.6em;
+ font-size: 0.97em;
+ white-space: pre-wrap;
+
+ &:last-child {
+ margin-bottom: 0.5em;
}
- background: #f5f5f5;
+
}
- &.current--user {
- .post-body {
- @include border-radius(4px);
- background: rgba(#000, 0.05);
+
+ span {
+ p:last-child {
+ margin-bottom: 0.5em;
}
}
- &.post--comment {
- &.other--root {
- .post-comment {
- border-left: 4px solid #EEE;
- padding: 4px 0 6px 10px;
- margin: 0 0 0 30px;
- @include border-radius(0 4px 4px 0);
+
+ &.post--root {
+
+ .comment-icon__container {
+ visibility: visible;
+ }
+
+ }
+
+ &.same--root {
+
+ &.same--user {
+ padding: 0 1em;
+
+ &:hover {
+
+ .post__time {
+
+ &:before {
+ @include opacity(0.5);
+ }
+
+ }
+
}
- .post-body {
- background: transparent;
+
+ .post__header {
margin: 0;
- padding: 1px 0px;
+ height: 0;
+
+ .col__name {
+ display: none;
+ }
+
+ .col__reply {
+ top: 6px;
+ }
+
+ }
+
+ .post__time {
+ top: 24px;
}
- &.current--user {
- .post-comment {
- background: #f5f5f5;
+
+ .post__time {
+ display: inline-block;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-size: 0;
+ position: absolute;
+ top: -3px;
+ left: 17px;
+ width: 30px;
+ height: 30px;
+ line-height: 37px;
+
+ &:before {
+ @include opacity(0);
+ content: "\f017";
+ content: "\f017";
+ font-size: 19px;
}
+
}
+
}
- &.same--root {
- .post-body {
- @include border-radius(0 4px 4px 0);
+
+ &.post--comment {
+
+ .post__link {
+ display: none;
}
- .post-body {
- border-left: 4px solid #EEE;
- width: 570px;
- margin-left: 30px;
- padding-left: 10px;
- .post-link {
+
+ .post__img {
+ img {
display: none;
}
}
+
+ }
+
+ &.post--comment {
+ .post__body {
+ border-left: 4px solid #ddd;
+ }
}
+
}
- &.same--root {
+
+ &.other--root {
+
.comment-icon__container {
- @include opacity(0);
+
+ &.icon--show {
+ visibility: visible;
+ }
+
}
- div.post-profile-img__container {
- height: 1px;
- .post-profile-img {
- visibility: hidden;
+
+ &.post--comment {
+
+ .post__header {
+
+ .col__reply {
+ top: 53px;
+ }
+
}
+
+ }
+
+ }
+
+ .post__content {
+ margin: 0 auto;
+ position: relative;
+ max-width: 1000px;
+ display: table;
+ width: 100%;
+
+ > div {
+ display: table-cell;
+ vertical-align: top;
}
- .post__content {
- padding: 0;
+
+ }
+
+ .post__header {
+ margin-bottom: 2px;
+
+ li {
+ display: inline-block;
}
- &.same--user {
- .post__content {
- padding-left: 46px;
+
+ .col__name {
+ margin-right: 7px;
+ font-weight: 600;
+ }
+
+ .col__reply {
+ position: absolute;
+ right: 0;
+ top: 30px;
+ width: 70px;
+ }
+
+ }
+
+ .post__img {
+ width: 46px;
+ img {
+ width: 36px;
+ height: 36px;
+ vertical-align: inherit;
+ @include border-radius(50px);
+ }
+ }
+
+ .dropdown {
+ display: inline-block;
+ visibility: hidden;
+ top: -1px;
+ float: right;
+
+ .dropdown-menu {
+ right: 0;
+ left: auto;
+ min-width: 130px;
+ padding: 2px 0;
+
+ li {
+ display: block;
+
}
- .post-header-post {
- visibility: hidden;
+
+ a {
+ padding: 5px 15px;
}
+
}
- }
- p {
- margin: 0 0 1em;
- line-height: 1.6em;
- font-size: 0.97em;
- white-space: pre-wrap;
+
}
- span {
- p:last-child {
- margin-bottom: 0.5em;
+ .post__dropdown {
+ &:after {
+ content: '[...]';
+ top: -1px;
+ position: relative;
}
}
+ .post__body {
+ word-wrap: break-word;
+ padding: 0.2em 0.5em 0em;
+ @include legacy-pie-clearfix;
+ width: calc(100% - 80px);
+ }
+
+ .post__link {
+ margin: 2px 0 5px;
+ font-size: 13px;
+ }
+
+ .post__time {
+ font-size: 13px;
+ @include opacity(0.6);
+ }
+
.post-loading-gif {
height:10px;
width:10px;
@@ -459,8 +612,15 @@ body.ios {
}
.comment-icon__container {
- margin-left: 7px;
+ margin-right: 7px;
fill: $primary-color;
+ display: inline-block;
+ visibility: hidden;
+
+ &.icon--visible {
+ visibility: visible;
+ }
+
.comment-icon {
display: inline-block;
top: 3px;
@@ -468,132 +628,13 @@ body.ios {
margin-right: 3px;
fill: inherit;
}
+
path {
fill: inherit;
}
+
}
- > div {
- &.post-profile-img__container {
- float: left;
- .post-profile-img {
- width: 36px;
- height: 36px;
- margin-right: 10px;
- vertical-align: inherit;
- @include border-radius(50px);
- }
- }
- &.post__content {
- padding-left: 46px;
- max-width: 100%;
- @include legacy-pie-clearfix;
- }
- }
- .post-image__columns {
- @include legacy-pie-clearfix;
- padding-bottom: 5px;
- }
- .post-info--hidden {
- display: none;
- }
- .post-body {
- position: relative;
- z-index: 1;
- max-width: 100%;
- 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;
- white-space: nowrap;
- cursor: pointer;
- }
- .post-body--code {
- font-size: .97em;
- position:relative;
- .post-body--code__language {
- position: absolute;
- right: 0;
- background: #fff;
- cursor: default;
- padding: 0.3em 0.5em 0.1em;
- border-bottom-left-radius: 4px;
- @include opacity(.3);
- }
- &.tex .katex-display {
- text-align: left;
- }
- code {
- white-space: pre;
- }
- }
- }
- .create-reply-form-wrap {
- width: 100%;
- margin-top: 5px;
- form {
- padding: 0;
- }
- .comment-btn {
- display: none;
- }
- }
- .post-header {
- position: relative;
- list-style-type: none;
- margin: 0 0 1px;
- padding-left: 0px;
- &.post-header-post {
- position: relative;
- z-index: 1;
- width: 200px;
- text-align: right;
- float: left;
- padding-right: 15px;
- }
- &.post-info {
- .post-profile-time {
- vertical-align: top;
- width: 150px;
- max-width: 220px;
- overflow: hidden;
- display: block;
- white-space: nowrap;
- text-overflow: ellipsis;
- margin: 0 0 0 50px;
- }
- }
- .post-header-col {
- vertical-align: top;
- display: inline-block;
- margin-right: 10px;
- &.post-header__reply {
- position: relative;
- top: -1px;
- min-width: 70px;
- .dropdown-menu {
- right: 0;
- left: auto;
- }
- .dropdown {
- min-width: 18px;
- display: inline-block;
- @include opacity(0);
- }
- .dropdown-toggle:after {
- content: '[...]';
- }
- }
- }
- .post-profile-time {
- @include opacity(0.5);
- }
- }
- .post-comment {
- }
+
.web-embed-data {
padding: 2px 0 0 10px;
background: #f9f9f9;
@@ -621,22 +662,6 @@ body.ios {
display: none;
}
}
- .msg-typing {
- margin-top: 2px;
- margin-left: 5px;
- color: #a8adb7;
- }
- .post-grey {
- color:lightgrey;
- }
- .post-reply {
- clear: both;
- width: 100%;
- word-break: break-word;
- .btn-file {
- width: 40px;
- }
- }
}
.bot-indicator {
diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss
index 54c3bcdf8..582b72c90 100644
--- a/web/sass-files/sass/partials/_post_right.scss
+++ b/web/sass-files/sass/partials/_post_right.scss
@@ -9,28 +9,31 @@
.post-right-comments-container {
position: relative;
- padding: 0;
+ padding: 0.7em 0 0;
}
.post {
+
&.post--root {
- padding: 1em 1em 0;
- margin: 0 0 1em;
- width: 100%;
- hr {
- border-color: #DDD;
- margin: 1em 0 0 0;
- }
+ padding-bottom: 0;
}
- .post-header {
- .post-profile-time {
- width: 200px;
- display: inline-block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+
+ .post__header {
+
+ .col__reply {
+ top: 0;
}
+
}
+
+ .post__body {
+ width: 100%;
+ }
+
+ }
+
+ hr {
+ margin-bottom: 0;
}
.post-create__container {
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index aad991035..368027cbf 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -8,96 +8,10 @@
}
}
}
- .post {
- &.post--comment {
- &.same--root {
- margin-left: 104px;
- padding-left: 10px;
- border-left: 4px solid #EEE;
- div.post-profile-img__container {
- .post-profile-img {
- display: none;
- }
- }
- .post__content {
- width: 825px;
- margin-left: 0;
- }
- .post-body {
- width: 736px;
- border: none;
- margin: 3px 0 0;
- }
- }
- &.other--root {
- .post-comment {
- margin-left: 0;
- }
- }
- &.same--root {
- margin-top: 10px;
- margin-bottom: 10px;
- }
- }
- .post-body {
- float: none;
- width: 750px;
- margin: 3px 0 0;
- }
- .post__content {
- width: 920px;
- }
- .post-header {
- &.post-header-post {
- z-index: inherit;
- width: auto;
- float: none;
- text-align: left;
- padding-right: 0;
- }
- &.post-info {
- display: none;
- float: none;
- }
- .post-header-col {
- &.post-header__reply {
- float: right;
- text-align: right;
- .reply-link {
- margin-right: 0;
- }
- .dropdown {
- margin-left: 10px;
- }
- }
- }
- .post-info--hidden {
- display: inline;
- .post-info {
- display: inline;
- .tooltip {
- margin-top: -25px;
- margin-left: 40px;
- }
- .post-profile-time {
- margin: 0;
- }
- }
- }
- }
- }
}
}
}
@media screen and (max-width: 1440px) {
- .post-create__container {
- .post-create-body {
- max-width: 810px;
- }
- }
- .channel-intro {
- max-width: 810px;
- }
.date-separator, .new-separator {
&.hovered--comment {
&:before, &:after {
@@ -105,105 +19,6 @@
}
}
}
- .post {
- &.same--root.same--user {
- .post-header-post {
- visibility: hidden;
- width: 100%;
- position: relative;
- top: -5px;
- .post-header-col.post-header__name {
- display: none;
- }
- }
- .post-body {
- top: -15px;
- margin-bottom: -10px;
- }
- &:hover .post-header-post {
- visibility: visible;
- }
- }
-
- &.post--comment {
- &.other--root {
- .post-comment {
- margin-left: 0;
- }
- }
- &.same--root {
- margin-top: 5px;
- margin-bottom: 5px;
- margin-left: 104px;
- padding-left: 10px;
- border-left: 4px solid #EEE;
- div.post-profile-img__container {
- .post-profile-img {
- display: none;
- }
- }
- .post-body {
- margin-left: 0;
- border-left: 0;
- }
- &.same--user {
- .post__content {
- margin-left: 0;
- padding-left: 0;
- }
- }
- }
- .post__content {
- width: 810px;
- }
- .post-body {
- width: 736px;
- border: none;
- margin: 3px 0 0;
- }
- }
- .post__content {
- width: 880px;
- }
- .post-header {
- &.post-header-post {
- z-index: inherit;
- width: auto;
- float: none;
- text-align: left;
- padding-right: 0;
- }
- &.post-info {
- display: none;
- float: none;
- }
- .post-header-col {
- &.post-header__reply {
- float: right;
- margin: 0;
- }
- }
- .post-info--hidden {
- display: inline;
- .post-info {
- display: inline;
- .tooltip {
- margin-top: -25px;
- margin-left: 40px;
- }
- .post-profile-time {
- width: auto;
- margin: 0;
- }
- }
- }
- }
- .post-body {
- margin: 3px 0 0;
- float: none;
- width: 750px;
- }
- }
}
@media screen and (max-width: 1140px) {
@@ -232,79 +47,38 @@
}
}
}
- .post {
- .post__content {
- width: 100%;
- }
- .post-header {
- .post-header-col {
- &.post-header__reply {
- .reply-link {
- margin-right: 0;
- }
- .dropdown {
- margin-left: 10px;
- }
- }
- }
- }
- }
}
@media screen and (max-width: 960px) {
- .post {
- .post-header .post-header-col.post-header__reply {
- .comment-icon__container__hide {
- display: none;
- }
- .dropdown {
- @include opacity(1);
- }
- .comment-icon__container__show {
- @include opacity(1);
- }
- }
- }
+
.signup-team__container {
font-size: 1em;
}
+
.post {
- .post-header {
- .post-header-col {
- &.post-header__reply {
- text-align: right;
- display: inline-block;
- }
+
+ .dropdown, .comment-icon__container {
+ visibility: visible;
+ }
+
+ .post__img {
+ width: 40px;
+
+ img {
+ width: 32px;
+ height: 32px;
}
}
- }
-}
-@media (min-width: 992px){
- .modal-lg {
- width: 700px;
}
-}
-@media screen and (min-width: 768px) {
- .second-bar {
- display: none;
+ .post-image__column {
+ width: 200px;
+ height: 95px;
}
-}
-@media screen and (max-height: 640px) {
- .signup-team__container {
- padding: 30px 0;
- margin-bottom: 30px;
- font-size: 0.9em;
- .signup-team__name {
- font-size: 2em;
- }
- }
-}
-@media screen and (max-width: 768px) {
.textarea-wrapper {
- .textbox-preview-link {
+ .textbox-preview-link, .textbox-help-link {
display: none;
}
}
@@ -385,35 +159,6 @@
}
}
}
- .post {
- &:hover {
- background: none;
- .post-header .post-header-col.post-header__reply {
- .dropdown-toggle:after {
- content: '...';
- }
- }
- }
- &.post--comment {
- &.same--root {
- margin-left: 25px;
- }
- &.other--root {
- margin-left: 0;
- &:hover {
- background: none;
- }
- }
- }
- .post-header .post-header-col.post-header__reply {
- .dropdown-toggle:after {
- content: '...';
- }
- }
- &.same--root.same--user .post__content{
- padding-left: 0;
- }
- }
.signup-team__container {
padding: 30px 0;
margin-bottom: 30px;
@@ -860,24 +605,6 @@
}
}
- .post {
- .post-profile-img__container {
- display: none;
- }
- &.post--comment {
- &.other--root {
- .post-comment {
- margin-left: 11px;
- }
- }
- }
- > div {
- &.post__content {
- padding: 0;
- }
- }
- }
-
.post-comments {
padding: 9px 21px 10px 10px !important;
}
@@ -958,3 +685,27 @@
}
}
}
+
+
+@media (min-width: 992px){
+ .modal-lg {
+ width: 700px;
+ }
+}
+
+@media screen and (min-width: 768px) {
+ .second-bar {
+ display: none;
+ }
+}
+
+@media screen and (max-height: 640px) {
+ .signup-team__container {
+ padding: 30px 0;
+ margin-bottom: 30px;
+ font-size: 0.9em;
+ .signup-team__name {
+ font-size: 2em;
+ }
+ }
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index bedf35376..27b55f214 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -1,5 +1,5 @@
#channel-header .search-bar__container {
- padding: 0 8px 0 0;
+ padding: 0 8px 0 3px;
}
.search-bar__container {
padding: 12px 8px 0 0;
diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss
index 2527eef28..43162831d 100644
--- a/web/sass-files/sass/partials/_sidebar--right.scss
+++ b/web/sass-files/sass/partials/_sidebar--right.scss
@@ -36,20 +36,6 @@
height: calc(100% - 56px);
@include border-radius(2px 0 0 0);
}
- .post {
- .post-header {
- .post-header-col {
- &.post-header__reply {
- min-width: 30px;
- text-align: right;
- float: right;
- }
- }
- }
- .post-body {
- margin: 3px 0 0;
- }
- }
.sidebar__overlay {
width: 100%;
height: 100%;