summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/channel.go2
-rw-r--r--api/post.go38
-rw-r--r--api/post_test.go70
-rw-r--r--api/web_conn.go5
-rw-r--r--api/web_hub.go8
-rw-r--r--api/web_team_hub.go9
-rw-r--r--model/client.go9
-rw-r--r--store/sql_post_store.go79
-rw-r--r--store/sql_post_store_test.go85
-rw-r--r--store/store.go1
-rw-r--r--web/react/components/channel_loader.jsx54
-rw-r--r--web/react/components/create_comment.jsx7
-rw-r--r--web/react/components/create_post.jsx4
-rw-r--r--web/react/components/delete_post_modal.jsx3
-rw-r--r--web/react/components/edit_post_modal.jsx2
-rw-r--r--web/react/components/post.jsx41
-rw-r--r--web/react/components/post_list.jsx831
-rw-r--r--web/react/components/post_right.jsx411
-rw-r--r--web/react/components/rhs_comment.jsx207
-rw-r--r--web/react/components/rhs_header_post.jsx81
-rw-r--r--web/react/components/rhs_root_post.jsx145
-rw-r--r--web/react/components/rhs_thread.jsx215
-rw-r--r--web/react/components/sidebar_right.jsx12
-rw-r--r--web/react/stores/post_store.jsx125
-rw-r--r--web/react/utils/async_client.jsx55
-rw-r--r--web/react/utils/client.jsx17
-rw-r--r--web/react/utils/utils.jsx2
-rw-r--r--web/sass-files/sass/partials/_post.scss3
28 files changed, 1631 insertions, 890 deletions
diff --git a/api/channel.go b/api/channel.go
index 5f3282072..b40366719 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -391,6 +391,8 @@ func JoinChannel(c *Context, channelId string, role string) {
c.Err = model.NewAppError("joinChannel", "Failed to send join request", "")
return
}
+
+ UpdateChannelAccessCacheAndForget(c.Session.TeamId, c.Session.UserId, channel.Id)
} else {
c.Err = model.NewAppError("joinChannel", "You do not have the appropriate permissions", "")
c.Err.StatusCode = http.StatusForbidden
diff --git a/api/post.go b/api/post.go
index c013df87f..5363fdf79 100644
--- a/api/post.go
+++ b/api/post.go
@@ -28,6 +28,7 @@ func InitPost(r *mux.Router) {
sr.Handle("/valet_create", ApiUserRequired(createValetPost)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updatePost)).Methods("POST")
sr.Handle("/posts/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET")
+ sr.Handle("/posts/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, false)).Methods("GET")
sr.Handle("/post/{post_id:[A-Za-z0-9]+}", ApiUserRequired(getPost)).Methods("GET")
sr.Handle("/post/{post_id:[A-Za-z0-9]+}/delete", ApiUserRequired(deletePost)).Methods("POST")
}
@@ -545,9 +546,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
rpost := result.Data.(*model.Post)
message := model.NewMessage(c.Session.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED)
- message.Add("post_id", rpost.Id)
- message.Add("channel_id", rpost.ChannelId)
- message.Add("message", rpost.Message)
+ message.Add("post", rpost.ToJson())
PublishAndForget(message)
@@ -603,6 +602,39 @@ func getPosts(c *Context, w http.ResponseWriter, r *http.Request) {
}
+func getPostsSince(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+
+ id := params["id"]
+ if len(id) != 26 {
+ c.SetInvalidParam("getPostsSince", "channelId")
+ return
+ }
+
+ time, err := strconv.ParseInt(params["time"], 10, 64)
+ if err != nil {
+ c.SetInvalidParam("getPostsSince", "time")
+ return
+ }
+
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId)
+ pchan := Srv.Store.Post().GetPostsSince(id, time)
+
+ if !c.HasPermissionsToChannel(cchan, "getPostsSince") {
+ return
+ }
+
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ list := result.Data.(*model.PostList)
+
+ w.Write([]byte(list.ToJson()))
+ }
+
+}
+
func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
diff --git a/api/post_test.go b/api/post_test.go
index cbba83af6..ac9adf358 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -351,6 +351,76 @@ func TestGetPosts(t *testing.T) {
}
}
+func TestGetPostsSince(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)
+
+ time.Sleep(10 * time.Millisecond)
+ post0 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post0 = Client.Must(Client.CreatePost(post0)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post1a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post1.Id}
+ post1a1 = Client.Must(Client.CreatePost(post1a1)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post3a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post3.Id}
+ post3a1 = Client.Must(Client.CreatePost(post3a1)).Data.(*model.Post)
+
+ r1 := Client.Must(Client.GetPostsSince(channel1.Id, post1.CreateAt)).Data.(*model.PostList)
+
+ if r1.Order[0] != post3a1.Id {
+ t.Fatal("wrong order")
+ }
+
+ if r1.Order[1] != post3.Id {
+ t.Fatal("wrong order")
+ }
+
+ if len(r1.Posts) != 5 {
+ t.Fatal("wrong size")
+ }
+
+ now := model.GetMillis()
+ r2 := Client.Must(Client.GetPostsSince(channel1.Id, now)).Data.(*model.PostList)
+
+ if len(r2.Posts) != 0 {
+ t.Fatal("should have been empty")
+ }
+
+ post2.Message = "new message"
+ Client.Must(Client.UpdatePost(post2))
+
+ r3 := Client.Must(Client.GetPostsSince(channel1.Id, now)).Data.(*model.PostList)
+
+ if len(r3.Order) != 2 { // 2 because deleted post is returned as well
+ t.Fatal("missing post update")
+ }
+}
+
func TestSearchPosts(t *testing.T) {
Setup()
diff --git a/api/web_conn.go b/api/web_conn.go
index 0990de8ef..4315f5650 100644
--- a/api/web_conn.go
+++ b/api/web_conn.go
@@ -121,6 +121,11 @@ func (c *WebConn) writePump() {
}
}
+func (c *WebConn) updateChannelAccessCache(channelId string) {
+ allowed := hasPermissionsToChannel(Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.UserId))
+ c.ChannelAccessCache[channelId] = allowed
+}
+
func hasPermissionsToChannel(sc store.StoreChannel) bool {
if cresult := <-sc; cresult.Err != nil {
return false
diff --git a/api/web_hub.go b/api/web_hub.go
index c7be19cac..44d405283 100644
--- a/api/web_hub.go
+++ b/api/web_hub.go
@@ -30,6 +30,14 @@ func PublishAndForget(message *model.Message) {
}()
}
+func UpdateChannelAccessCacheAndForget(teamId, userId, channelId string) {
+ go func() {
+ if nh, ok := hub.teamHubs[teamId]; ok {
+ nh.UpdateChannelAccessCache(userId, channelId)
+ }
+ }()
+}
+
func (h *Hub) Register(webConn *WebConn) {
h.register <- webConn
}
diff --git a/api/web_team_hub.go b/api/web_team_hub.go
index 7a63b84d1..31c8dfedf 100644
--- a/api/web_team_hub.go
+++ b/api/web_team_hub.go
@@ -77,3 +77,12 @@ func (h *TeamHub) Start() {
}
}()
}
+
+func (h *TeamHub) UpdateChannelAccessCache(userId string, channelId string) {
+ for webCon := range h.connections {
+ if webCon.UserId == userId {
+ webCon.updateChannelAccessCache(channelId)
+ break
+ }
+ }
+}
diff --git a/model/client.go b/model/client.go
index 9fcb06cf8..c355b90f5 100644
--- a/model/client.go
+++ b/model/client.go
@@ -522,6 +522,15 @@ func (c *Client) GetPosts(channelId string, offset int, limit int, etag string)
}
}
+func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError) {
+ if r, err := c.DoGet(fmt.Sprintf("/channels/%v/posts/%v", channelId, time), "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) {
if r, err := c.DoGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil {
return nil, err
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index 479caf838..4ea28507b 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -7,7 +7,6 @@ import (
"fmt"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "strconv"
"strings"
)
@@ -158,14 +157,6 @@ func (s SqlPostStore) Get(id string) StoreChannel {
result.Err = model.NewAppError("SqlPostStore.GetPost", "We couldn't get the post", "id="+id+err.Error())
}
- if post.ImgCount > 0 {
- post.Filenames = []string{}
- for i := 0; int64(i) < post.ImgCount; i++ {
- fileUrl := "/api/v1/files/get_image/" + post.ChannelId + "/" + post.Id + "/" + strconv.Itoa(i+1) + ".png"
- post.Filenames = append(post.Filenames, fileUrl)
- }
- }
-
pl.AddPost(&post)
pl.AddOrder(id)
@@ -265,25 +256,11 @@ func (s SqlPostStore) GetPosts(channelId string, offset int, limit int) StoreCha
list := &model.PostList{Order: make([]string, 0, len(posts))}
for _, p := range posts {
- if p.ImgCount > 0 {
- p.Filenames = []string{}
- for i := 0; int64(i) < p.ImgCount; i++ {
- fileUrl := "/api/v1/files/get_image/" + p.ChannelId + "/" + p.Id + "/" + strconv.Itoa(i+1) + ".png"
- p.Filenames = append(p.Filenames, fileUrl)
- }
- }
list.AddPost(p)
list.AddOrder(p.Id)
}
for _, p := range parents {
- if p.ImgCount > 0 {
- p.Filenames = []string{}
- for i := 0; int64(i) < p.ImgCount; i++ {
- fileUrl := "/api/v1/files/get_image/" + p.ChannelId + "/" + p.Id + "/" + strconv.Itoa(i+1) + ".png"
- p.Filenames = append(p.Filenames, fileUrl)
- }
- }
list.AddPost(p)
}
@@ -299,6 +276,62 @@ func (s SqlPostStore) GetPosts(channelId string, offset int, limit int) StoreCha
return storeChannel
}
+func (s SqlPostStore) GetPostsSince(channelId string, time int64) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var posts []*model.Post
+ _, err := s.GetReplica().Select(&posts,
+ `(SELECT
+ *
+ FROM
+ Posts
+ WHERE
+ (UpdateAt > :Time
+ AND ChannelId = :ChannelId)
+ LIMIT 100)
+ UNION
+ (SELECT
+ *
+ FROM
+ Posts
+ WHERE
+ Id
+ IN
+ (SELECT * FROM (SELECT
+ RootId
+ FROM
+ Posts
+ WHERE
+ UpdateAt > :Time
+ AND ChannelId = :ChannelId
+ LIMIT 100) temp_tab))
+ ORDER BY CreateAt DESC`,
+ map[string]interface{}{"ChannelId": channelId, "Time": time})
+
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.GetPostsSince", "We couldn't get the posts for the channel", "channelId="+channelId+err.Error())
+ } else {
+
+ list := &model.PostList{Order: make([]string, 0, len(posts))}
+
+ for _, p := range posts {
+ list.AddPost(p)
+ list.AddOrder(p.Id)
+ }
+
+ result.Data = list
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlPostStore) getRootPosts(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 336a20d98..8854fb5c4 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -383,6 +383,91 @@ func TestPostStoreGetPostsWtihDetails(t *testing.T) {
}
}
+func TestPostStoreGetPostsSince(t *testing.T) {
+ Setup()
+ o0 := &model.Post{}
+ o0.ChannelId = model.NewId()
+ o0.UserId = model.NewId()
+ o0.Message = "a" + model.NewId() + "b"
+ o0 = (<-store.Post().Save(o0)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ 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)
+ time.Sleep(2 * time.Millisecond)
+
+ 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)
+ time.Sleep(2 * time.Millisecond)
+
+ o2a := &model.Post{}
+ o2a.ChannelId = o1.ChannelId
+ o2a.UserId = model.NewId()
+ o2a.Message = "a" + model.NewId() + "b"
+ o2a.ParentId = o1.Id
+ o2a.RootId = o1.Id
+ o2a = (<-store.Post().Save(o2a)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o3 := &model.Post{}
+ o3.ChannelId = o1.ChannelId
+ o3.UserId = model.NewId()
+ o3.Message = "a" + model.NewId() + "b"
+ o3.ParentId = o1.Id
+ o3.RootId = o1.Id
+ o3 = (<-store.Post().Save(o3)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o4 := &model.Post{}
+ o4.ChannelId = o1.ChannelId
+ o4.UserId = model.NewId()
+ o4.Message = "a" + model.NewId() + "b"
+ o4 = (<-store.Post().Save(o4)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o5 := &model.Post{}
+ o5.ChannelId = o1.ChannelId
+ o5.UserId = model.NewId()
+ o5.Message = "a" + model.NewId() + "b"
+ o5.ParentId = o4.Id
+ o5.RootId = o4.Id
+ o5 = (<-store.Post().Save(o5)).Data.(*model.Post)
+
+ r1 := (<-store.Post().GetPostsSince(o1.ChannelId, o1.CreateAt)).Data.(*model.PostList)
+
+ if r1.Order[0] != o5.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r1.Order[1] != o4.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r1.Order[2] != o3.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r1.Order[3] != o2a.Id {
+ t.Fatal("invalid order")
+ }
+
+ if len(r1.Posts) != 6 {
+ t.Fatal("wrong size")
+ }
+
+ if r1.Posts[o1.Id].Message != o1.Message {
+ t.Fatal("Missing parent")
+ }
+}
+
func TestPostStoreSearch(t *testing.T) {
Setup()
diff --git a/store/store.go b/store/store.go
index 8dbf12b55..271caa366 100644
--- a/store/store.go
+++ b/store/store.go
@@ -75,6 +75,7 @@ type PostStore interface {
Get(id string) StoreChannel
Delete(postId string, time int64) StoreChannel
GetPosts(channelId string, offset int, limit int) StoreChannel
+ GetPostsSince(channelId string, time int64) StoreChannel
GetEtag(channelId string) StoreChannel
Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel
}
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index 525b67b5c..0fa433383 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -5,63 +5,83 @@
to the server on page load. This is to prevent other React controls from spamming
AsyncClient with requests. */
-var BrowserStore = require('../stores/browser_store.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var SocketStore = require('../stores/socket_store.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var PostStore = require('../stores/post_store.jsx');
+var UserStore = require('../stores/user_store.jsx');
var Constants = require('../utils/constants.jsx');
+var utils = require('../utils/utils.jsx');
+
module.exports = React.createClass({
componentDidMount: function() {
- /* Start initial aysnc loads */
+ /* Initial aysnc loads */
AsyncClient.getMe();
- AsyncClient.getPosts(true, ChannelStore.getCurrentId(), Constants.POST_CHUNK_SIZE);
+ AsyncClient.getPosts(ChannelStore.getCurrentId());
AsyncClient.getChannels(true, true);
AsyncClient.getChannelExtraInfo(true);
AsyncClient.findTeams();
AsyncClient.getStatuses();
AsyncClient.getMyTeam();
- /* End of async loads */
/* Perform pending post clean-up */
PostStore.clearPendingPosts();
- /* End pending post clean-up */
- /* Start interval functions */
+ /* Set up interval functions */
setInterval(
function pollStatuses() {
AsyncClient.getStatuses();
}, 30000);
- /* End interval functions */
- /* Start device tracking setup */
+ /* Device tracking setup */
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
if (iOS) {
$('body').addClass('ios');
}
- /* End device tracking setup */
- /* Start window active tracking setup */
+ /* Set up tracking for whether the window is active */
window.isActive = true;
- $(window).focus(function() {
+ $(window).focus(function windowFocus() {
AsyncClient.updateLastViewedAt();
window.isActive = true;
});
- $(window).blur(function() {
+ $(window).blur(function windowBlur() {
window.isActive = false;
});
- /* End window active tracking setup */
/* Start global change listeners setup */
- SocketStore.addChangeListener(this._onSocketChange);
- /* End global change listeners setup */
+ SocketStore.addChangeListener(this.onSocketChange);
+
+ /* Update CSS classes to match user theme */
+ var user = UserStore.getCurrentUser();
+
+ if (user.props && user.props.theme) {
+ utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';');
+ utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';');
+ utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';');
+ utils.changeCss('.mention', 'background: ' + user.props.theme + ';');
+ utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';');
+ utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}');
+ utils.changeCss('.search-item-container:hover', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) + ';');
+ }
+
+ if (user.props.theme !== '#000000' && user.props.theme !== '#585858') {
+ utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) + ';');
+ utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;');
+ } else if (user.props.theme === '#000000') {
+ utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +50) + ';');
+ $('.team__header').addClass('theme--black');
+ } else if (user.props.theme === '#585858') {
+ utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +10) + ';');
+ $('.team__header').addClass('theme--gray');
+ }
},
- _onSocketChange: function(msg) {
- if (msg && msg.user_id) {
+ onSocketChange: function(msg) {
+ if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
UserStore.setStatus(msg.user_id, 'online');
}
},
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 1de768872..c2b7e222f 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -29,8 +29,6 @@ module.exports = React.createClass({
return;
}
- this.setState({submitting: true, serverError: null});
-
var post = {};
post.filenames = [];
post.message = this.state.messageText;
@@ -57,11 +55,10 @@ module.exports = React.createClass({
PostStore.storePendingPost(post);
PostStore.storeCommentDraft(this.props.rootId, null);
- this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
client.createPost(post, ChannelStore.getCurrent(),
function(data) {
- AsyncClient.getPosts(true, this.props.channelId);
+ AsyncClient.getPosts(this.props.channelId);
var channel = ChannelStore.get(this.props.channelId);
var member = ChannelStore.getMember(this.props.channelId);
@@ -91,6 +88,8 @@ module.exports = React.createClass({
this.setState(state);
}.bind(this)
);
+
+ this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
},
commentMsgKeyPress: function(e) {
if (e.which === 13 && !e.shiftKey && !e.altKey) {
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index efaa40577..9aef2dea4 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -82,7 +82,7 @@ module.exports = React.createClass({
client.createPost(post, channel,
function(data) {
this.resizePostHolder();
- AsyncClient.getPosts(true);
+ AsyncClient.getPosts();
var member = ChannelStore.getMember(channel.id);
member.msg_count = channel.total_msg_count;
@@ -112,8 +112,6 @@ module.exports = React.createClass({
}.bind(this)
);
}
-
- $('.post-list-holder-by-time').perfectScrollbar('update');
},
componentDidUpdate: function() {
this.resizePostHolder();
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index 1b6a7e162..55d6f509c 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -44,7 +44,8 @@ module.exports = React.createClass({
}
}
}
- AsyncClient.getPosts(true, this.state.channel_id);
+ PostStore.removePost(this.state.post_id, this.state.channel_id);
+ AsyncClient.getPosts(this.state.channel_id);
}.bind(this),
function(err) {
AsyncClient.dispatchError(err, "deletePost");
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index 2d865a45d..a801cfc1b 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -25,7 +25,7 @@ module.exports = React.createClass({
Client.updatePost(updatedPost,
function(data) {
- AsyncClient.getPosts(true, this.state.channel_id);
+ AsyncClient.getPosts(this.state.channel_id);
window.scrollTo(0, 0);
}.bind(this),
function(err) {
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index cc2e37fa8..acc2b51d2 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -11,11 +11,12 @@ var ChannelStore = require('../stores/channel_store.jsx');
var client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var ActionTypes = Constants.ActionTypes;
+var utils = require('../utils/utils.jsx');
var PostInfo = require('./post_info.jsx');
module.exports = React.createClass({
- displayName: "Post",
+ displayName: 'Post',
handleCommentClick: function(e) {
e.preventDefault();
@@ -43,7 +44,7 @@ module.exports = React.createClass({
var post = this.props.post;
client.createPost(post, post.channel_id,
function(data) {
- AsyncClient.getPosts(true);
+ AsyncClient.getPosts();
var channel = ChannelStore.get(post.channel_id);
var member = ChannelStore.getMember(post.channel_id);
@@ -67,6 +68,13 @@ module.exports = React.createClass({
PostStore.updatePendingPost(post);
this.forceUpdate();
},
+ shouldComponentUpdate: function(nextProps) {
+ if (!utils.areStatesEqual(nextProps.post, this.props.post)) {
+ return true;
+ }
+
+ return false;
+ },
getInitialState: function() {
return { };
},
@@ -90,16 +98,16 @@ module.exports = React.createClass({
var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null;
- var rootUser = this.props.sameRoot ? "same--root" : "other--root";
+ var rootUser = this.props.sameRoot ? 'same--root' : 'other--root';
- var postType = "";
- if (type != "Post"){
- postType = "post--comment";
+ var postType = '';
+ if (type != 'Post'){
+ postType = 'post--comment';
}
- var currentUserCss = "";
+ var currentUserCss = '';
if (UserStore.getCurrentId() === post.user_id) {
- currentUserCss = "current--user";
+ currentUserCss = 'current--user';
}
var userProfile = UserStore.getProfile(post.user_id);
@@ -109,18 +117,23 @@ module.exports = React.createClass({
timestamp = userProfile.update_at;
}
+ var sameUserClass = '';
+ if (this.props.sameUser) {
+ sameUserClass = 'same--user';
+ }
+
return (
<div>
- <div id={post.id} className={"post " + this.props.sameUser + " " + rootUser + " " + postType + " " + currentUserCss}>
+ <div id={post.id} className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}>
{ !this.props.hideProfilePic ?
- <div className="post-profile-img__container">
- <img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image?time=" + timestamp} height="36" width="36" />
+ <div className='post-profile-img__container'>
+ <img className='post-profile-img' src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} height='36' width='36' />
</div>
: null }
- <div className="post__content">
- <PostHeader ref="header" post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} isLastComment={this.props.isLastComment} />
+ <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" />
+ <PostInfo ref='info' post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} allowReply='true' />
</div>
</div>
</div>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 7748f5c2a..865a22dbd 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -15,124 +15,116 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
-function getStateFromStores() {
- var channel = ChannelStore.getCurrent();
-
- if (channel == null) {
- channel = {};
+export default class PostList extends React.Component {
+ constructor() {
+ super();
+
+ this.gotMorePosts = false;
+ this.scrolled = false;
+ this.prevScrollTop = 0;
+ this.seenNewMessages = false;
+ this.isUserScroll = true;
+ this.userHasSeenNew = false;
+
+ this.onChange = this.onChange.bind(this);
+ this.onTimeChange = this.onTimeChange.bind(this);
+ this.onSocketChange = this.onSocketChange.bind(this);
+ this.createChannelIntroMessage = this.createChannelIntroMessage.bind(this);
+ this.loadMorePosts = this.loadMorePosts.bind(this);
+
+ this.state = this.getStateFromStores();
+ this.state.numToDisplay = Constants.POST_CHUNK_SIZE;
}
+ getStateFromStores() {
+ var channel = ChannelStore.getCurrent();
- var postList = PostStore.getCurrentPosts();
- var deletedPosts = PostStore.getUnseenDeletedPosts(channel.id);
-
- if (deletedPosts && Object.keys(deletedPosts).length > 0) {
- for (var pid in deletedPosts) {
- postList.posts[pid] = deletedPosts[pid];
- postList.order.unshift(pid);
+ if (channel == null) {
+ channel = {};
}
- postList.order.sort(function postSort(a, b) {
- if (postList.posts[a].create_at > postList.posts[b].create_at) {
- return -1;
- }
- if (postList.posts[a].create_at < postList.posts[b].create_at) {
- return 1;
- }
- return 0;
- });
- }
+ var postList = PostStore.getCurrentPosts();
- var pendingPostList = PostStore.getPendingPosts(channel.id);
+ if (postList != null) {
+ var deletedPosts = PostStore.getUnseenDeletedPosts(channel.id);
- if (pendingPostList) {
- postList.order = pendingPostList.order.concat(postList.order);
- for (var ppid in pendingPostList.posts) {
- postList.posts[ppid] = pendingPostList.posts[ppid];
- }
- }
+ if (deletedPosts && Object.keys(deletedPosts).length > 0) {
+ for (var pid in deletedPosts) {
+ postList.posts[pid] = deletedPosts[pid];
+ postList.order.unshift(pid);
+ }
- return {
- postList: postList,
- channel: channel
- };
-}
+ postList.order.sort(function postSort(a, b) {
+ if (postList.posts[a].create_at > postList.posts[b].create_at) {
+ return -1;
+ }
+ if (postList.posts[a].create_at < postList.posts[b].create_at) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+
+ var pendingPostList = PostStore.getPendingPosts(channel.id);
-module.exports = React.createClass({
- displayName: 'PostList',
- scrollPosition: 0,
- preventScrollTrigger: false,
- gotMorePosts: false,
- oldScrollHeight: 0,
- oldZoom: 0,
- scrolledToNew: false,
- componentDidMount: function() {
- var user = UserStore.getCurrentUser();
- if (user.props && user.props.theme) {
- utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';');
- utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';');
- utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';');
- utils.changeCss('.mention', 'background: ' + user.props.theme + ';');
- utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';');
- utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}');
- utils.changeCss('.search-item-container:hover', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) + ';');
- utils.changeCss('.nav-pills__unread-indicator', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) + ';');
+ if (pendingPostList) {
+ postList.order = pendingPostList.order.concat(postList.order);
+ for (var ppid in pendingPostList.posts) {
+ postList.posts[ppid] = pendingPostList.posts[ppid];
+ }
+ }
}
- if (user.props.theme !== '#000000' && user.props.theme !== '#585858') {
- utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) + ';');
- utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;');
- } else if (user.props.theme === '#000000') {
- utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +50) + ';');
- $('.team__header').addClass('theme--black');
- } else if (user.props.theme === '#585858') {
- utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +10) + ';');
- $('.team__header').addClass('theme--gray');
+ var lastViewed = Number.MAX_VALUE;
+
+ if (ChannelStore.getCurrentMember() != null) {
+ lastViewed = ChannelStore.getCurrentMember().last_viewed_at;
}
+ return {
+ postList: postList,
+ channel: channel,
+ lastViewed: lastViewed
+ };
+ }
+ componentDidMount() {
PostStore.addChangeListener(this.onChange);
ChannelStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onTimeChange);
SocketStore.addChangeListener(this.onSocketChange);
- $('.post-list-holder-by-time').perfectScrollbar();
-
- this.resize();
-
- var postHolder = $('.post-list-holder-by-time')[0];
- this.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight();
- this.oldScrollHeight = postHolder.scrollHeight;
- this.oldZoom = (window.outerWidth - 8) / window.innerWidth;
+ var postHolder = $('.post-list-holder-by-time');
$('.modal').on('show.bs.modal', function onShow() {
$('.modal-body').css('overflow-y', 'auto');
$('.modal-body').css('max-height', $(window).height() * 0.7);
});
- var self = this;
$(window).resize(function resize() {
- $(postHolder).perfectScrollbar('update');
-
- // this only kind of works, detecting zoom in browsers is a nightmare
- var newZoom = (window.outerWidth - 8) / window.innerWidth;
+ if ($('#create_post').length > 0) {
+ var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
+ postHolder.css('height', height + 'px');
+ }
- if (self.scrollPosition >= postHolder.scrollHeight || (self.oldScrollHeight !== postHolder.scrollHeight && self.scrollPosition >= self.oldScrollHeight) || self.oldZoom !== newZoom) {
- self.resize();
+ if (!this.scrolled) {
+ this.scrollToBottom();
}
+ }.bind(this));
- self.oldZoom = newZoom;
+ postHolder.scroll(function scroll() {
+ var position = postHolder.scrollTop() + postHolder.height() + 14;
+ var bottom = postHolder[0].scrollHeight;
- if ($('#create_post').length > 0) {
- var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
- $('.post-list-holder-by-time').css('height', height + 'px');
+ if (position >= bottom) {
+ this.scrolled = false;
+ } else {
+ this.scrolled = true;
}
- });
- $(postHolder).scroll(function scroll() {
- if (!self.preventScrollTrigger) {
- self.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight();
+ if (this.isUserScroll) {
+ this.userHasSeenNew = true;
}
- self.preventScrollTrigger = false;
- });
+ this.isUserScroll = true;
+ }.bind(this));
$('body').on('click.userpopover', function popOver(e) {
if ($(e.target).attr('data-toggle') !== 'popover' &&
@@ -163,76 +155,101 @@ module.exports = React.createClass({
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
}
});
- },
- componentDidUpdate: function() {
- this.resize();
- var postHolder = $('.post-list-holder-by-time')[0];
- this.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight();
- this.oldScrollHeight = postHolder.scrollHeight;
+
+ this.scrollToBottom();
+ setTimeout(this.scrollToBottom, 100);
+ }
+ componentDidUpdate(prevProps, prevState) {
$('.post-list__content div .post').removeClass('post--last');
$('.post-list__content div:last-child .post').addClass('post--last');
- },
- componentWillUnmount: function() {
+
+ if (this.state.postList == null || prevState.postList == null) {
+ this.scrollToBottom();
+ return;
+ }
+
+ var order = this.state.postList.order || [];
+ var posts = this.state.postList.posts || {};
+ var oldOrder = prevState.postList.order || [];
+ var oldPosts = prevState.postList.posts || {};
+ var userId = UserStore.getCurrentId();
+ var firstPost = posts[order[0]] || {};
+ var isNewPost = oldOrder.indexOf(order[0]) === -1;
+
+ if (this.state.channel.id !== prevState.channel.id) {
+ this.scrollToBottom();
+ } else if (oldOrder.length === 0) {
+ this.scrollToBottom();
+
+ // the user is scrolled to the bottom
+ } else if (!this.scrolled) {
+ this.scrollToBottom();
+
+ // there's a new post and
+ // it's by the user and not a comment
+ } else if (isNewPost &&
+ userId === firstPost.user_id &&
+ !utils.isComment(firstPost)) {
+ this.state.lastViewed = utils.getTimestamp();
+ this.scrollToBottom(true);
+
+ // the user clicked 'load more messages'
+ } else if (this.gotMorePosts) {
+ var lastPost = oldPosts[oldOrder[prevState.numToDisplay]];
+ $('#' + lastPost.id)[0].scrollIntoView();
+ } else {
+ this.scrollTo(this.prevScrollTop);
+ }
+ }
+ componentWillUpdate() {
+ var postHolder = $('.post-list-holder-by-time');
+ this.prevScrollTop = postHolder.scrollTop();
+ }
+ componentWillUnmount() {
PostStore.removeChangeListener(this.onChange);
ChannelStore.removeChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onTimeChange);
SocketStore.removeChangeListener(this.onSocketChange);
$('body').off('click.userpopover');
$('.modal').off('show.bs.modal');
- },
- resize: function() {
- var postHolder = $('.post-list-holder-by-time')[0];
- this.preventScrollTrigger = true;
- if (this.gotMorePosts) {
- this.gotMorePosts = false;
- $(postHolder).scrollTop($(postHolder).scrollTop() + (postHolder.scrollHeight - this.oldScrollHeight));
- } else if ($('#new_message')[0] && !this.scrolledToNew) {
- $(postHolder).scrollTop($(postHolder).scrollTop() + $('#new_message').offset().top - 63);
- this.scrolledToNew = true;
+ }
+ scrollTo(val) {
+ this.isUserScroll = false;
+ var postHolder = $('.post-list-holder-by-time');
+ postHolder[0].scrollTop = val;
+ }
+ scrollToBottom(force) {
+ this.isUserScroll = false;
+ var postHolder = $('.post-list-holder-by-time');
+ if ($('#new_message')[0] && !this.userHasSeenNew && !force) {
+ $('#new_message')[0].scrollIntoView();
} else {
- $(postHolder).scrollTop(postHolder.scrollHeight);
+ postHolder.addClass('hide-scroll');
+ postHolder[0].scrollTop = postHolder[0].scrollHeight;
+ postHolder.removeClass('hide-scroll');
}
- $(postHolder).perfectScrollbar('update');
- },
- onChange: function() {
- var newState = getStateFromStores();
+ }
+ onChange() {
+ var newState = this.getStateFromStores();
if (!utils.areStatesEqual(newState, this.state)) {
- if (this.state.postList && this.state.postList.order) {
- if (this.state.channel.id === newState.channel.id && this.state.postList.order.length !== newState.postList.order.length && newState.postList.order.length > Constants.POST_CHUNK_SIZE) {
- this.gotMorePosts = true;
- }
- }
if (this.state.channel.id !== newState.channel.id) {
PostStore.clearUnseenDeletedPosts(this.state.channel.id);
- this.scrolledToNew = false;
+ this.userHasSeenNew = false;
+ newState.numToDisplay = Constants.POST_CHUNK_SIZE;
+ } else {
+ newState.lastViewed = this.state.lastViewed;
}
+
this.setState(newState);
}
- },
- onSocketChange: function(msg) {
+ }
+ onSocketChange(msg) {
var postList;
var post;
- if (msg.action === 'posted') {
+ if (msg.action === 'posted' || msg.action === 'post_edited') {
post = JSON.parse(msg.props.post);
PostStore.storePost(post);
- } else if (msg.action === 'post_edited') {
- if (this.state.channel.id === msg.channel_id) {
- postList = this.state.postList;
- if (!(msg.props.post_id in postList.posts)) {
- return;
- }
-
- post = postList.posts[msg.props.post_id];
- post.message = msg.props.message;
-
- postList.posts[post.id] = post;
- this.setState({postList: postList});
-
- PostStore.storePosts(msg.channel_id, postList);
- } else {
- AsyncClient.getPosts(true, msg.channel_id);
- }
} else if (msg.action === 'post_deleted') {
var activeRoot = $(document.activeElement).closest('.comment-create-body')[0];
var activeRootPostId = '';
@@ -244,16 +261,8 @@ module.exports = React.createClass({
postList = this.state.postList;
PostStore.storeUnseenDeletedPost(post);
-
- if (postList.posts[post.id]) {
- delete postList.posts[post.id];
- var index = postList.order.indexOf(post.id);
- if (index > -1) {
- postList.order.splice(index, 1);
- }
-
- PostStore.storePosts(msg.channel_id, postList);
- }
+ PostStore.removePost(post, true);
+ PostStore.emitChange();
if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) {
$('#post_deleted').modal('show');
@@ -261,8 +270,8 @@ module.exports = React.createClass({
} else if (msg.action === 'new_user') {
AsyncClient.getProfiles();
}
- },
- onTimeChange: function() {
+ }
+ onTimeChange() {
if (!this.state.postList) {
return;
}
@@ -273,271 +282,331 @@ module.exports = React.createClass({
}
this.refs[id].forceUpdateInfo();
}
- },
- getMorePosts: function(e) {
- e.preventDefault();
-
- if (!this.state.postList) {
- return;
- }
-
- var posts = this.state.postList.posts;
- var order = this.state.postList.order;
- var channelId = this.state.channel.id;
-
- $(this.refs.loadmore.getDOMNode()).text('Retrieving more messages...');
-
- var self = this;
- var currentPos = $('.post-list').scrollTop;
-
- Client.getPosts(
- channelId,
- order.length,
- Constants.POST_CHUNK_SIZE,
- function success(data) {
- $(self.refs.loadmore.getDOMNode()).text('Load more messages');
-
- if (!data) {
- return;
- }
+ }
+ createDMIntroMessage(channel) {
+ var teammate = utils.getDirectTeammate(channel.id);
- if (data.order.length === 0) {
- return;
- }
+ if (teammate) {
+ var teammateName = teammate.username;
+ if (teammate.nickname.length > 0) {
+ teammateName = teammate.nickname;
+ }
- var postList = {};
- postList.posts = $.extend(posts, data.posts);
- postList.order = order.concat(data.order);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POSTS,
- id: channelId,
- postList: postList
- });
-
- Client.getProfiles();
- $('.post-list').scrollTop(currentPos);
- },
- function fail(err) {
- $(self.refs.loadmore.getDOMNode()).text('Load more messages');
- AsyncClient.dispatchError(err, 'getPosts');
- }
+ return (
+ <div className='channel-intro'>
+ <div className='post-profile-img__container channel-intro-img'>
+ <img
+ className='post-profile-img'
+ src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at}
+ height='50'
+ width='50'
+ />
+ </div>
+ <div className='channel-intro-profile'>
+ <strong><UserProfile userId={teammate.id} /></strong>
+ </div>
+ <p className='channel-intro-text'>
+ {'This is the start of your private message history with ' + teammateName + '.'}<br/>
+ {'Private messages and files shared here are not shown to people outside this area.'}
+ </p>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-desc={channel.description}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>Set a description
+ </a>
+ </div>
);
- },
- getInitialState: function() {
- return getStateFromStores();
- },
- render: function() {
- var order = [];
- var posts;
+ }
- var lastViewed = Number.MAX_VALUE;
+ return (
+ <div className='channel-intro'>
+ <p className='channel-intro-text'>{'This is the start of your private message history with this ' + strings.Team + 'mate. Private messages and files shared here are not shown to people outside this area.'}</p>
+ </div>
+ );
+ }
+ createChannelIntroMessage(channel) {
+ if (channel.type === 'D') {
+ return this.createDMIntroMessage(channel);
+ } else if (ChannelStore.isDefault(channel)) {
+ return this.createDefaultIntroMessage(channel);
+ } else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
+ return this.createOffTopicIntroMessage(channel);
+ } else if (channel.type === 'O' || channel.type === 'P') {
+ return this.createStandardIntroMessage(channel);
+ }
+ }
+ createDefaultIntroMessage(channel) {
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>Beginning of {channel.display_name}</h4>
+ <p className='channel-intro__content'>
+ Welcome to {channel.display_name}!
+ <br/><br/>
+ This is the first channel {strings.Team}mates see when they
+ <br/>
+ sign up - use it for posting updates everyone needs to know.
+ <br/><br/>
+ To create a new channel or join an existing one, go to
+ <br/>
+ the Left Hand Sidebar under “Channels” and click “More…”.
+ <br/>
+ </p>
+ </div>
+ );
+ }
+ createOffTopicIntroMessage(channel) {
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>Beginning of {channel.display_name}</h4>
+ <p className='channel-intro__content'>
+ {'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'}
+ <br/>
+ </p>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-desc={channel.description}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>Set a description
+ </a>
+ </div>
+ );
+ }
+ getChannelCreator(channel) {
+ if (channel.creator_id.length > 0) {
+ var creator = UserStore.getProfile(channel.creator_id);
+ if (creator) {
+ return creator.username;
+ }
+ }
- if (ChannelStore.getCurrentMember() != null) {
- lastViewed = ChannelStore.getCurrentMember().last_viewed_at;
+ var members = ChannelStore.getCurrentExtraInfo().members;
+ for (var i = 0; i < members.length; i++) {
+ if (members[i].roles.indexOf('admin') > -1) {
+ return members[i].username;
+ }
+ }
+ }
+ createStandardIntroMessage(channel) {
+ var uiName = channel.display_name;
+ var creatorName = '';
+
+ var uiType;
+ var memberMessage;
+ if (channel.type === 'P') {
+ uiType = 'private group';
+ memberMessage = ' Only invited members can see this private group.';
+ } else {
+ uiType = 'channel';
+ memberMessage = ' Any member can join and read this channel.';
}
- if (this.state.postList != null) {
- posts = this.state.postList.posts;
- order = this.state.postList.order;
+ var createMessage;
+ if (creatorName !== '') {
+ createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{utils.displayDate(channel.create_at)}</strong></span>);
+ } else {
+ createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.';
}
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>Beginning of {uiName}</h4>
+ <p className='channel-intro__content'>
+ {createMessage}
+ {memberMessage}
+ <br/>
+ </p>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-desc={channel.description}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>Set a description
+ </a>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#channel_invite'
+ >
+ <i className='fa fa-user-plus'></i>Invite others to this {uiType}
+ </a>
+ </div>
+ );
+ }
+ createPosts(posts, order) {
+ var postCtls = [];
+ var previousPostDay = new Date(0);
+ var userId = UserStore.getCurrentId();
+
var renderedLastViewed = false;
- var userId = '';
- if (UserStore.getCurrentId()) {
- userId = UserStore.getCurrentId();
- } else {
- return <div/>;
+ var numToDisplay = this.state.numToDisplay;
+ if (order.length - 1 < numToDisplay) {
+ numToDisplay = order.length - 1;
}
- var channel = this.state.channel;
-
- var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>;
+ for (var i = numToDisplay; i >= 0; i--) {
+ var post = posts[order[i]];
+ var parentPost = posts[post.parent_id];
- var userStyle = {color: UserStore.getCurrentUser().props.theme};
+ var sameUser = false;
+ var sameRoot = false;
+ var hideProfilePic = false;
+ var prevPost = posts[order[i + 1]];
- if (channel != null) {
- if (order.length > 0 && order.length % Constants.POST_CHUNK_SIZE === 0) {
- moreMessages = <a ref='loadmore' className='more-messages-text theme' href='#' onClick={this.getMorePosts}>Load more messages</a>;
- } else if (channel.type === 'D') {
- var teammate = utils.getDirectTeammate(channel.id);
-
- if (teammate) {
- var teammateName = teammate.username;
- if (teammate.nickname.length > 0) {
- teammateName = teammate.nickname;
- }
+ if (prevPost) {
+ sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
- moreMessages = (
- <div className='channel-intro'>
- <div className='post-profile-img__container channel-intro-img'>
- <img className='post-profile-img' src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at} height='50' width='50' />
- </div>
- <div className='channel-intro-profile'>
- <strong><UserProfile userId={teammate.id} /></strong>
- </div>
- <p className='channel-intro-text'>
- This is the start of your private message history with <strong>{teammateName}</strong>.<br/>
- Private messages and files shared here are not shown to people outside this area.
- </p>
- <a className='intro-links' href='#' style={userStyle} data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}><i className='fa fa-pencil'></i>Set a description</a>
- </div>
- );
- } else {
- moreMessages = (
- <div className='channel-intro'>
- <p className='channel-intro-text'>{'This is the start of your private message history with this ' + strings.Team + 'mate. Private messages and files shared here are not shown to people outside this area.'}</p>
- </div>
- );
- }
- } else if (channel.type === 'P' || channel.type === 'O') {
- var uiName = channel.display_name;
- var creatorName = '';
-
- if (channel.creator_id.length > 0) {
- var creator = UserStore.getProfile(channel.creator_id);
- if (creator) {
- creatorName = creator.username;
- }
- }
+ sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
- if (creatorName === '') {
- var members = ChannelStore.getCurrentExtraInfo().members;
- for (var i = 0; i < members.length; i++) {
- if (members[i].roles.indexOf('admin') > -1) {
- creatorName = members[i].username;
- break;
- }
- }
- }
+ // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
+ hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post);
+ }
- if (ChannelStore.isDefault(channel)) {
- moreMessages = (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>Beginning of {uiName}</h4>
- <p className='channel-intro__content'>
- Welcome to <strong>{uiName}</strong>!
- <br/><br/>
- This is the first channel {strings.Team}mates see when they
- <br/>
- sign up - use it for posting updates everyone needs to know.
- <br/><br/>
- To create a new channel or join an existing one, go to
- <br/>
- the Left Hand Sidebar under “Channels” and click “More…”.
- <br/>
- </p>
- </div>
- );
- } else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
- moreMessages = (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>Beginning of {uiName}</h4>
- <p className='channel-intro__content'>
- This is the start of <strong>{uiName}</strong>, a channel for non-work-related conversations.
- <br/>
- </p>
- <a className='intro-links' href='#' style={userStyle} data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={uiName} data-channelid={channel.id}><i className='fa fa-pencil'></i>Set a description</a>
- </div>
- );
- } else {
- var uiType;
- var memberMessage;
- if (channel.type === 'P') {
- uiType = 'private group';
- memberMessage = ' Only invited members can see this private group.';
- } else {
- uiType = 'channel';
- memberMessage = ' Any member can join and read this channel.';
- }
+ // check if it's the last comment in a consecutive string of comments on the same post
+ // it is the last comment if it is last post in the channel or the next post has a different root post
+ var isLastComment = utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
+
+ var postCtl = (
+ <Post
+ key={post.id}
+ ref={post.id}
+ sameUser={sameUser}
+ sameRoot={sameRoot}
+ post={post}
+ parentPost={parentPost}
+ posts={posts}
+ hideProfilePic={hideProfilePic}
+ isLastComment={isLastComment}
+ />
+ );
- var createMessage;
- if (creatorName !== '') {
- createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{utils.displayDate(channel.create_at)}</strong></span>);
- } else {
- createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.';
- }
+ let currentPostDay = utils.getDateForUnixTicks(post.create_at);
+ if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
+ postCtls.push(
+ <div
+ key={currentPostDay.toDateString()}
+ className='date-separator'
+ >
+ <hr className='separator__hr' />
+ <div className='separator__text'>{currentPostDay.toDateString()}</div>
+ </div>
+ );
+ }
- moreMessages = (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>Beginning of {uiName}</h4>
- <p className='channel-intro__content'>
- {createMessage}
- {memberMessage}
- <br/>
- </p>
- <a className='intro-links' href='#' style={userStyle} data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}><i className='fa fa-pencil'></i>Set a description</a>
- <a className='intro-links' href='#' style={userStyle} data-toggle='modal' data-target='#channel_invite'><i className='fa fa-user-plus'></i>Invite others to this {uiType}</a>
- </div>
- );
- }
+ if (post.user_id !== userId && post.create_at > this.state.lastViewed && !renderedLastViewed) {
+ renderedLastViewed = true;
+ postCtls.push(
+ <div
+ id='new_message'
+ key='unviewed'
+ className='new-separator'
+ >
+ <hr
+ className='separator__hr'
+ />
+ <div className='separator__text'>New Messages</div>
+ </div>
+ );
}
+ postCtls.push(postCtl);
+ previousPostDay = currentPostDay;
}
- var postCtls = [];
+ return postCtls;
+ }
+ loadMorePosts() {
+ if (this.state.postList == null) {
+ return;
+ }
- if (posts) {
- var previousPostDay = new Date(0);
- var currentPostDay;
-
- for (var i = order.length - 1; i >= 0; i--) {
- var post = posts[order[i]];
- var parentPost = null;
- if (post.parent_id) {
- parentPost = posts[post.parent_id];
- }
+ var posts = this.state.postList.posts;
+ var order = this.state.postList.order;
+ var channelId = this.state.channel.id;
- var sameUser = '';
- var sameRoot = false;
- var hideProfilePic = false;
- var prevPost;
- if (i < order.length - 1) {
- prevPost = posts[order[i + 1]];
- }
+ $(this.refs.loadmore.getDOMNode()).text('Retrieving more messages...');
- if (prevPost) {
- if ((prevPost.user_id === post.user_id) && (post.create_at - prevPost.create_at <= 1000 * 60 * 5)) {
- sameUser = 'same--user';
- }
- sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
+ Client.getPostsPage(
+ channelId,
+ order.length,
+ Constants.POST_CHUNK_SIZE,
+ function success(data) {
+ $(this.refs.loadmore.getDOMNode()).text('Load more messages');
+ this.gotMorePosts = true;
+ this.setState({numToDisplay: this.state.numToDisplay + Constants.POST_CHUNK_SIZE});
- // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
- hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post);
+ if (!data) {
+ return;
}
- // check if it's the last comment in a consecutive string of comments on the same post
- // it is the last comment if it is last post in the channel or the next post has a different root post
- var isLastComment = utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
+ if (data.order.length === 0) {
+ return;
+ }
- var postCtl = (
- <Post ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id}
- posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment}
- />
- );
+ var postList = {};
+ postList.posts = $.extend(posts, data.posts);
+ postList.order = order.concat(data.order);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POSTS,
+ id: channelId,
+ post_list: postList
+ });
+
+ Client.getProfiles();
+ }.bind(this),
+ function fail(err) {
+ $(this.refs.loadmore.getDOMNode()).text('Load more messages');
+ AsyncClient.dispatchError(err, 'getPosts');
+ }.bind(this)
+ );
+ }
+ render() {
+ var order = [];
+ var posts;
+ var channel = this.state.channel;
- currentPostDay = utils.getDateForUnixTicks(post.create_at);
- if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
- postCtls.push(
- <div key={currentPostDay.toDateString()} className='date-separator'>
- <hr className='separator__hr' />
- <div className='separator__text'>{currentPostDay.toDateString()}</div>
- </div>
- );
- }
+ if (this.state.postList != null) {
+ posts = this.state.postList.posts;
+ order = this.state.postList.order;
+ }
- if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) {
- renderedLastViewed = true;
- postCtls.push(
- <div key='unviewed' className='new-separator'>
- <hr id='new_message' className='separator__hr' />
- <div className='separator__text'>New Messages</div>
- </div>
- );
- }
- postCtls.push(postCtl);
- previousPostDay = currentPostDay;
+ var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>;
+ if (channel != null) {
+ if (order.length > this.state.numToDisplay) {
+ moreMessages = (
+ <a
+ ref='loadmore'
+ className='more-messages-text theme'
+ href='#'
+ onClick={this.loadMorePosts}
+ >
+ Load more messages
+ </a>
+ );
+ } else {
+ moreMessages = this.createChannelIntroMessage(channel);
}
+ }
+
+ var postCtls = [];
+ if (posts) {
+ postCtls = this.createPosts(posts, order);
} else {
postCtls.push(<LoadingScreen position='absolute' />);
}
@@ -553,4 +622,4 @@ module.exports = React.createClass({
</div>
);
}
-});
+}
diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx
deleted file mode 100644
index fb45ad28e..000000000
--- a/web/react/components/post_right.jsx
+++ /dev/null
@@ -1,411 +0,0 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var PostStore = require('../stores/post_store.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
-var UserProfile = require('./user_profile.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var utils = require('../utils/utils.jsx');
-var SearchBox = require('./search_bar.jsx');
-var CreateComment = require('./create_comment.jsx');
-var Constants = require('../utils/constants.jsx');
-var FileAttachmentList = require('./file_attachment_list.jsx');
-var FileUploadOverlay = require('./file_upload_overlay.jsx');
-var client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var ActionTypes = Constants.ActionTypes;
-
-RhsHeaderPost = React.createClass({
- handleClose: function(e) {
- e.preventDefault();
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_SEARCH,
- results: null
- });
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_SEARCH_TERM,
- term: null,
- do_search: false,
- is_mention_search: false
- });
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST_SELECTED,
- results: null
- });
- },
- handleBack: function(e) {
- e.preventDefault();
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_SEARCH_TERM,
- term: this.props.fromSearch,
- do_search: true,
- is_mention_search: this.props.isMentionSearch
- });
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST_SELECTED,
- results: null
- });
- },
- render: function() {
- var back;
- if (this.props.fromSearch) {
- back = <a href='#' onClick={this.handleBack} className='sidebar--right__back'><i className='fa fa-chevron-left'></i></a>;
- }
-
- return (
- <div className='sidebar--right__header'>
- <span className='sidebar--right__title'>{back}Message Details</span>
- <button type='button' className='sidebar--right__close' aria-label='Close' onClick={this.handleClose}></button>
- </div>
- );
- }
-});
-
-RootPost = React.createClass({
- render: function() {
- var post = this.props.post;
- var message = utils.textToJsx(post.message);
- var isOwner = UserStore.getCurrentId() === post.user_id;
- var timestamp = UserStore.getProfile(post.user_id).update_at;
- var channel = ChannelStore.get(post.channel_id);
-
- var type = 'Post';
- if (post.root_id.length > 0) {
- type = 'Comment';
- }
-
- var currentUserCss = '';
- if (UserStore.getCurrentId() === post.user_id) {
- currentUserCss = 'current--user';
- }
-
- var channelName;
- if (channel) {
- if (channel.type === 'D') {
- channelName = 'Private Message';
- } else {
- channelName = channel.display_name;
- }
- }
-
- var ownerOptions;
- if (isOwner) {
- ownerOptions = (
- <div>
- <a href='#' className='dropdown-toggle theme' type='button' data-toggle='dropdown' aria-expanded='false' />
- <ul className='dropdown-menu' role='menu'>
- <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#edit_post' data-refoucsid='#reply_textbox' data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>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}>Delete</a></li>
- </ul>
- </div>
- );
- }
-
- var fileAttachment;
- if (post.filenames && post.filenames.length > 0) {
- fileAttachment = (
- <FileAttachmentList
- filenames={post.filenames}
- modalId={'rhs_view_image_modal_' + post.id}
- channelId={post.channel_id}
- userId={post.user_id} />
- );
- }
-
- return (
- <div className={'post post--root ' + currentUserCss}>
- <div className='post-right-channel__name'>{ channelName }</div>
- <div className='post-profile-img__container'>
- <img className='post-profile-img' src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} 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-right-root-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'>
- <p>{message}</p>
- {fileAttachment}
- </div>
- </div>
- <hr />
- </div>
- );
- }
-});
-
-CommentPost = React.createClass({
- retryComment: function(e) {
- e.preventDefault();
-
- var post = this.props.post;
- client.createPost(post, post.channel_id,
- function success(data) {
- AsyncClient.getPosts(true);
-
- var channel = ChannelStore.get(post.channel_id);
- var member = ChannelStore.getMember(post.channel_id);
- member.msg_count = channel.total_msg_count;
- member.last_viewed_at = (new Date()).getTime();
- ChannelStore.setChannelMember(member);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST,
- post: data
- });
- }.bind(this),
- function fail() {
- post.state = Constants.POST_FAILED;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- }.bind(this)
- );
-
- post.state = Constants.POST_LOADING;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- },
- render: function() {
- var post = this.props.post;
-
- var currentUserCss = '';
- if (UserStore.getCurrentId() === post.user_id) {
- currentUserCss = 'current--user';
- }
-
- var isOwner = UserStore.getCurrentId() === post.user_id;
-
- var type = 'Post';
- if (post.root_id.length > 0) {
- type = 'Comment';
- }
-
- var message = utils.textToJsx(post.message);
- var timestamp = UserStore.getCurrentUser().update_at;
-
- var loading;
- var postClass = '';
- if (post.state === Constants.POST_FAILED) {
- postClass += ' post-fail';
- loading = <a className='theme post-retry pull-right' href='#' onClick={this.retryComment}>Retry</a>;
- } else if (post.state === Constants.POST_LOADING) {
- postClass += ' post-waiting';
- loading = <img className='post-loading-gif pull-right' src='/static/images/load.gif'/>;
- }
-
- var ownerOptions;
- if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) {
- ownerOptions = (
- <div className='dropdown' onClick={function(e){$('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50);}}>
- <a href='#' className='dropdown-toggle theme' type='button' data-toggle='dropdown' aria-expanded='false' />
- <ul className='dropdown-menu' role='menu'>
- <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#edit_post' data-refoucsid='#reply_textbox' data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>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={0}>Delete</a></li>
- </ul>
- </div>
- );
- }
-
- var fileAttachment;
- if (post.filenames && post.filenames.length > 0) {
- fileAttachment = (
- <FileAttachmentList
- filenames={post.filenames}
- modalId={'rhs_comment_view_image_modal_' + post.id}
- channelId={post.channel_id}
- userId={post.user_id} />
- );
- }
-
- 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} 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-right-comment-time'>{utils.displayCommentDateTime(post.create_at)}</time></li>
- <li className='post-header-col post-header__reply'>
- {ownerOptions}
- </li>
- </ul>
- <div className='post-body'>
- <p className={postClass}>{loading}{message}</p>
- {fileAttachment}
- </div>
- </div>
- </div>
- );
- }
-});
-
-function getStateFromStores() {
- var postList = PostStore.getSelectedPost();
- if (!postList || postList.order.length < 1) {
- return {postList: {}};
- }
-
- var channelId = postList.posts[postList.order[0]].channel_id;
- var pendingPostList = PostStore.getPendingPosts(channelId);
-
- if (pendingPostList) {
- for (var pid in pendingPostList.posts) {
- postList.posts[pid] = pendingPostList.posts[pid];
- }
- }
-
- return {postList: postList};
-}
-
-module.exports = React.createClass({
- componentDidMount: function() {
- PostStore.addSelectedPostChangeListener(this.onChange);
- PostStore.addChangeListener(this.onChangeAll);
- UserStore.addStatusesChangeListener(this.onTimeChange);
- this.resize();
- var self = this;
- $(window).resize(function() {
- self.resize();
- });
- },
- componentDidUpdate: function() {
- $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
- $('.post-right__scroll').perfectScrollbar('update');
- this.resize();
- },
- componentWillUnmount: function() {
- PostStore.removeSelectedPostChangeListener(this.onChange);
- PostStore.removeChangeListener(this.onChangeAll);
- UserStore.removeStatusesChangeListener(this.onTimeChange);
- },
- onChange: function() {
- if (this.isMounted()) {
- var newState = getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
- this.setState(newState);
- }
- }
- },
- onChangeAll: function() {
- if (this.isMounted()) {
- // if something was changed in the channel like adding a
- // comment or post then lets refresh the sidebar list
- var currentSelected = PostStore.getSelectedPost();
- if (!currentSelected || currentSelected.order.length === 0) {
- return;
- }
-
- var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id);
-
- if (!currentPosts || currentPosts.order.length === 0) {
- return;
- }
-
- if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) {
- currentSelected.posts = {};
- for (var postId in currentPosts.posts) {
- currentSelected.posts[postId] = currentPosts.posts[postId];
- }
-
- PostStore.storeSelectedPost(currentSelected);
- }
-
- this.setState(getStateFromStores());
- }
- },
- onTimeChange: function() {
- for (var id in this.state.postList.posts) {
- if (!this.refs[id]) {
- continue;
- }
- this.refs[id].forceUpdate();
- }
- },
- getInitialState: function() {
- return getStateFromStores();
- },
- resize: function() {
- var height = $(window).height() - $('#error_bar').outerHeight() - 100;
- $('.post-right__scroll').css('height', height + 'px');
- $('.post-right__scroll').scrollTop(100000);
- $('.post-right__scroll').perfectScrollbar();
- $('.post-right__scroll').perfectScrollbar('update');
- },
- render: function() {
- var postList = this.state.postList;
-
- if (postList == null) {
- return (
- <div></div>
- );
- }
-
- var selectedPost = postList.posts[postList.order[0]];
- var rootPost = null;
-
- if (selectedPost.root_id === '') {
- rootPost = selectedPost;
- } else {
- rootPost = postList.posts[selectedPost.root_id];
- }
-
- var postsArray = [];
-
- for (var postId in postList.posts) {
- var cpost = postList.posts[postId];
- if (cpost.root_id === rootPost.id) {
- postsArray.push(cpost);
- }
- }
-
- postsArray.sort(function postSort(a, b) {
- if (a.create_at < b.create_at) {
- return -1;
- }
- if (a.create_at > b.create_at) {
- return 1;
- }
- return 0;
- });
-
- var currentId = UserStore.getCurrentId();
- var searchForm;
- if (currentId != null) {
- searchForm = <SearchBox />;
- }
-
- return (
- <div className='post-right__container'>
- <FileUploadOverlay
- overlayType='right' />
- <div className='search-bar__container sidebar--right__search-header'>{searchForm}</div>
- <div className='sidebar-right__body'>
- <RhsHeaderPost fromSearch={this.props.fromSearch} isMentionSearch={this.props.isMentionSearch} />
- <div className='post-right__scroll'>
- <RootPost post={rootPost} commentCount={postsArray.length}/>
- <div className='post-right-comments-container'>
- {postsArray.map(function mapPosts(comPost) {
- return <CommentPost ref={comPost.id} key={comPost.id} post={comPost} selected={(comPost.id === selectedPost.id)} />;
- })}
- </div>
- <div className='post-create__container'>
- <CreateComment channelId={rootPost.channel_id} rootId={rootPost.id} />
- </div>
- </div>
- </div>
- </div>
- );
- }
-});
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
new file mode 100644
index 000000000..7df2fed9e
--- /dev/null
+++ b/web/react/components/rhs_comment.jsx
@@ -0,0 +1,207 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var PostStore = require('../stores/post_store.jsx');
+var ChannelStore = require('../stores/channel_store.jsx');
+var UserProfile = require('./user_profile.jsx');
+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 FileAttachmentList = require('./file_attachment_list.jsx');
+var client = require('../utils/client.jsx');
+var AsyncClient = require('../utils/async_client.jsx');
+var ActionTypes = Constants.ActionTypes;
+
+export default class RhsComment extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.retryComment = this.retryComment.bind(this);
+
+ this.state = {};
+ }
+ retryComment(e) {
+ e.preventDefault();
+
+ var post = this.props.post;
+ client.createPost(post, post.channel_id,
+ function success(data) {
+ AsyncClient.getPosts(post.channel_id);
+
+ var channel = ChannelStore.get(post.channel_id);
+ var member = ChannelStore.getMember(post.channel_id);
+ member.msg_count = channel.total_msg_count;
+ member.last_viewed_at = (new Date()).getTime();
+ ChannelStore.setChannelMember(member);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POST,
+ post: data
+ });
+ },
+ function fail() {
+ post.state = Constants.POST_FAILED;
+ PostStore.updatePendingPost(post);
+ this.forceUpdate();
+ }.bind(this)
+ );
+
+ post.state = Constants.POST_LOADING;
+ PostStore.updatePendingPost(post);
+ this.forceUpdate();
+ }
+ shouldComponentUpdate(nextProps) {
+ if (!utils.areStatesEqual(nextProps.post, this.props.post)) {
+ return true;
+ }
+
+ return false;
+ }
+ render() {
+ var post = this.props.post;
+
+ var currentUserCss = '';
+ if (UserStore.getCurrentId() === post.user_id) {
+ currentUserCss = 'current--user';
+ }
+
+ var isOwner = UserStore.getCurrentId() === post.user_id;
+
+ var type = 'Post';
+ if (post.root_id.length > 0) {
+ type = 'Comment';
+ }
+
+ var message = utils.textToJsx(post.message);
+ var timestamp = UserStore.getCurrentUser().update_at;
+
+ var loading;
+ var postClass = '';
+ if (post.state === Constants.POST_FAILED) {
+ postClass += ' post-fail';
+ loading = (
+ <a
+ className='theme post-retry pull-right'
+ href='#'
+ onClick={this.retryComment}
+ >
+ Retry
+ </a>
+ );
+ } else if (post.state === Constants.POST_LOADING) {
+ postClass += ' post-waiting';
+ loading = (
+ <img
+ className='post-loading-gif pull-right'
+ src='/static/images/load.gif'
+ />
+ );
+ }
+
+ var ownerOptions;
+ if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) {
+ ownerOptions = (
+ <div
+ className='dropdown'
+ onClick={
+ function scroll() {
+ $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50);
+ }
+ }
+ >
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ data-toggle='dropdown'
+ aria-expanded='false'
+ />
+ <ul
+ className='dropdown-menu'
+ role='menu'
+ >
+ <li role='presentation'>
+ <a
+ href='#'
+ role='menuitem'
+ data-toggle='modal'
+ data-target='#edit_post'
+ data-title={type}
+ data-message={post.message}
+ data-postid={post.id}
+ data-channelid={post.channel_id}
+ >
+ 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={0}
+ >
+ Delete
+ </a>
+ </li>
+ </ul>
+ </div>
+ );
+ }
+
+ var fileAttachment;
+ if (post.filenames && post.filenames.length > 0) {
+ fileAttachment = (
+ <FileAttachmentList
+ filenames={post.filenames}
+ modalId={'rhs_comment_view_image_modal_' + post.id}
+ channelId={post.channel_id}
+ userId={post.user_id} />
+ );
+ }
+
+ 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}
+ 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-right-comment-time'>
+ {utils.displayCommentDateTime(post.create_at)}
+ </time>
+ </li>
+ <li className='post-header-col post-header__reply'>
+ {ownerOptions}
+ </li>
+ </ul>
+ <div className='post-body'>
+ <p className={postClass}>{loading}{message}</p>
+ {fileAttachment}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+RhsComment.defaultProps = {
+ post: null
+};
+RhsComment.propTypes = {
+ post: React.PropTypes.object
+};
diff --git a/web/react/components/rhs_header_post.jsx b/web/react/components/rhs_header_post.jsx
new file mode 100644
index 000000000..4cf4231e9
--- /dev/null
+++ b/web/react/components/rhs_header_post.jsx
@@ -0,0 +1,81 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+var Constants = require('../utils/constants.jsx');
+var ActionTypes = Constants.ActionTypes;
+
+export default class RhsHeaderPost extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleClose = this.handleClose.bind(this);
+ this.handleBack = this.handleBack.bind(this);
+
+ this.state = {};
+ }
+ handleClose(e) {
+ e.preventDefault();
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_SEARCH,
+ results: null
+ });
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POST_SELECTED,
+ results: null
+ });
+ }
+ handleBack(e) {
+ e.preventDefault();
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_SEARCH_TERM,
+ term: this.props.fromSearch,
+ do_search: true,
+ is_mention_search: this.props.isMentionSearch
+ });
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POST_SELECTED,
+ results: null
+ });
+ }
+ render() {
+ var back;
+ if (this.props.fromSearch) {
+ back = (
+ <a
+ href='#'
+ onClick={this.handleBack}
+ className='sidebar--right__back'
+ >
+ <i className='fa fa-chevron-left'></i>
+ </a>
+ );
+ }
+
+ return (
+ <div className='sidebar--right__header'>
+ <span className='sidebar--right__title'>{back}Message Details</span>
+ <button
+ type='button'
+ className='sidebar--right__close'
+ aria-label='Close'
+ onClick={this.handleClose}
+ >
+ </button>
+ </div>
+ );
+ }
+}
+
+RhsHeaderPost.defaultProps = {
+ isMentionSearch: false,
+ fromSearch: ''
+};
+RhsHeaderPost.propTypes = {
+ isMentionSearch: React.PropTypes.bool,
+ fromSearch: React.PropTypes.string
+};
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
new file mode 100644
index 000000000..a407e6470
--- /dev/null
+++ b/web/react/components/rhs_root_post.jsx
@@ -0,0 +1,145 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var ChannelStore = require('../stores/channel_store.jsx');
+var UserProfile = require('./user_profile.jsx');
+var UserStore = require('../stores/user_store.jsx');
+var utils = require('../utils/utils.jsx');
+var FileAttachmentList = require('./file_attachment_list.jsx');
+
+export default class RhsRootPost extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+ shouldComponentUpdate(nextProps) {
+ if (!utils.areStatesEqual(nextProps.post, this.props.post)) {
+ return true;
+ }
+
+ return false;
+ }
+ render() {
+ var post = this.props.post;
+ var message = utils.textToJsx(post.message);
+ var isOwner = UserStore.getCurrentId() === post.user_id;
+ var timestamp = UserStore.getProfile(post.user_id).update_at;
+ var channel = ChannelStore.get(post.channel_id);
+
+ var type = 'Post';
+ if (post.root_id.length > 0) {
+ type = 'Comment';
+ }
+
+ var currentUserCss = '';
+ if (UserStore.getCurrentId() === post.user_id) {
+ currentUserCss = 'current--user';
+ }
+
+ var channelName;
+ if (channel) {
+ if (channel.type === 'D') {
+ channelName = 'Private Message';
+ } else {
+ channelName = channel.display_name;
+ }
+ }
+
+ var ownerOptions;
+ if (isOwner) {
+ ownerOptions = (
+ <div>
+ <a href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ data-toggle='dropdown'
+ aria-expanded='false'
+ />
+ <ul
+ className='dropdown-menu'
+ role='menu'
+ >
+ <li role='presentation'>
+ <a
+ href='#'
+ role='menuitem'
+ data-toggle='modal'
+ data-target='#edit_post'
+ data-title={type}
+ data-message={post.message}
+ data-postid={post.id}
+ data-channelid={post.channel_id}
+ >
+ 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}
+ >
+ Delete
+ </a>
+ </li>
+ </ul>
+ </div>
+ );
+ }
+
+ var fileAttachment;
+ if (post.filenames && post.filenames.length > 0) {
+ fileAttachment = (
+ <FileAttachmentList
+ filenames={post.filenames}
+ modalId={'rhs_view_image_modal_' + post.id}
+ channelId={post.channel_id}
+ userId={post.user_id} />
+ );
+ }
+
+ return (
+ <div className={'post post--root ' + currentUserCss}>
+ <div className='post-right-channel__name'>{channelName}</div>
+ <div className='post-profile-img__container'>
+ <img
+ className='post-profile-img'
+ src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp}
+ 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-right-root-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'>
+ <p>{message}</p>
+ {fileAttachment}
+ </div>
+ </div>
+ <hr />
+ </div>
+ );
+ }
+}
+
+RhsRootPost.defaultProps = {
+ post: null,
+ commentCount: 0
+};
+RhsRootPost.propTypes = {
+ post: React.PropTypes.object,
+ commentCount: React.PropTypes.number
+};
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
new file mode 100644
index 000000000..adddeccf0
--- /dev/null
+++ b/web/react/components/rhs_thread.jsx
@@ -0,0 +1,215 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var PostStore = require('../stores/post_store.jsx');
+var UserStore = require('../stores/user_store.jsx');
+var utils = require('../utils/utils.jsx');
+var SearchBox = require('./search_bar.jsx');
+var CreateComment = require('./create_comment.jsx');
+var RhsHeaderPost = require('./rhs_header_post.jsx');
+var RootPost = require('./rhs_root_post.jsx');
+var Comment = require('./rhs_comment.jsx');
+var Constants = require('../utils/constants.jsx');
+var FileUploadOverlay = require('./file_upload_overlay.jsx');
+
+export default class RhsThread extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+ this.onChangeAll = this.onChangeAll.bind(this);
+ this.onTimeChange = this.onTimeChange.bind(this);
+
+ this.state = this.getStateFromStores();
+ }
+ getStateFromStores() {
+ var postList = PostStore.getSelectedPost();
+ if (!postList || postList.order.length < 1) {
+ return {postList: {}};
+ }
+
+ var channelId = postList.posts[postList.order[0]].channel_id;
+ var pendingPostList = PostStore.getPendingPosts(channelId);
+
+ if (pendingPostList) {
+ for (var pid in pendingPostList.posts) {
+ postList.posts[pid] = pendingPostList.posts[pid];
+ }
+ }
+
+ return {postList: postList};
+ }
+ componentDidMount() {
+ PostStore.addSelectedPostChangeListener(this.onChange);
+ PostStore.addChangeListener(this.onChangeAll);
+ UserStore.addStatusesChangeListener(this.onTimeChange);
+ this.resize();
+ $(window).resize(function resize() {
+ this.resize();
+ }.bind(this));
+ }
+ componentDidUpdate() {
+ $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
+ $('.post-right__scroll').perfectScrollbar('update');
+ this.resize();
+ }
+ componentWillUnmount() {
+ PostStore.removeSelectedPostChangeListener(this.onChange);
+ PostStore.removeChangeListener(this.onChangeAll);
+ UserStore.removeStatusesChangeListener(this.onTimeChange);
+ }
+ onChange() {
+ var newState = this.getStateFromStores();
+ if (!utils.areStatesEqual(newState, this.state)) {
+ this.setState(newState);
+ }
+ }
+ onChangeAll() {
+ // if something was changed in the channel like adding a
+ // comment or post then lets refresh the sidebar list
+ var currentSelected = PostStore.getSelectedPost();
+ if (!currentSelected || currentSelected.order.length === 0) {
+ return;
+ }
+
+ var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id);
+
+ if (!currentPosts || currentPosts.order.length === 0) {
+ return;
+ }
+
+ if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) {
+ currentSelected.posts = {};
+ for (var postId in currentPosts.posts) {
+ currentSelected.posts[postId] = currentPosts.posts[postId];
+ }
+
+ PostStore.storeSelectedPost(currentSelected);
+ }
+
+ var newState = this.getStateFromStores();
+ if (!utils.areStatesEqual(newState, this.state)) {
+ this.setState(newState);
+ }
+ }
+ onTimeChange() {
+ for (var id in this.state.postList.posts) {
+ if (!this.refs[id]) {
+ continue;
+ }
+ this.refs[id].forceUpdate();
+ }
+ }
+ resize() {
+ var height = $(window).height() - $('#error_bar').outerHeight() - 100;
+ $('.post-right__scroll').css('height', height + 'px');
+ $('.post-right__scroll').scrollTop(100000);
+ $('.post-right__scroll').perfectScrollbar();
+ $('.post-right__scroll').perfectScrollbar('update');
+ }
+ render() {
+ var postList = this.state.postList;
+
+ if (postList == null) {
+ return (
+ <div></div>
+ );
+ }
+
+ var selectedPost = postList.posts[postList.order[0]];
+ var rootPost = null;
+
+ if (selectedPost.root_id === '') {
+ rootPost = selectedPost;
+ } else {
+ rootPost = postList.posts[selectedPost.root_id];
+ }
+
+ var postsArray = [];
+
+ for (var postId in postList.posts) {
+ var cpost = postList.posts[postId];
+ if (cpost.root_id === rootPost.id) {
+ postsArray.push(cpost);
+ }
+ }
+
+ // sort failed posts to bottom, followed by pending, and then regular posts
+ postsArray.sort(function postSort(a, b) {
+ if ((a.state === Constants.POST_LOADING || a.state === Constants.POST_FAILED) && (b.state !== Constants.POST_LOADING && b.state !== Constants.POST_FAILED)) {
+ return 1;
+ }
+ if ((a.state !== Constants.POST_LOADING && a.state !== Constants.POST_FAILED) && (b.state === Constants.POST_LOADING || b.state === Constants.POST_FAILED)) {
+ return -1;
+ }
+
+ if (a.state === Constants.POST_LOADING && b.state === Constants.POST_FAILED) {
+ return -1;
+ }
+ if (a.state === Constants.POST_FAILED && b.state === Constants.POST_LOADING) {
+ return 1;
+ }
+
+ if (a.create_at < b.create_at) {
+ return -1;
+ }
+ if (a.create_at > b.create_at) {
+ return 1;
+ }
+ return 0;
+ });
+
+ var currentId = UserStore.getCurrentId();
+ var searchForm;
+ if (currentId != null) {
+ searchForm = <SearchBox />;
+ }
+
+ return (
+ <div className='post-right__container'>
+ <FileUploadOverlay
+ overlayType='right' />
+ <div className='search-bar__container sidebar--right__search-header'>{searchForm}</div>
+ <div className='sidebar-right__body'>
+ <RhsHeaderPost
+ fromSearch={this.props.fromSearch}
+ isMentionSearch={this.props.isMentionSearch}
+ />
+ <div className='post-right__scroll'>
+ <RootPost
+ post={rootPost}
+ commentCount={postsArray.length}
+ />
+ <div className='post-right-comments-container'>
+ {postsArray.map(function mapPosts(comPost) {
+ return (
+ <Comment
+ ref={comPost.id}
+ key={comPost.id}
+ post={comPost}
+ selected={(comPost.id === selectedPost.id)}
+ />
+ );
+ })}
+ </div>
+ <div className='post-create__container'>
+ <CreateComment
+ channelId={rootPost.channel_id}
+ rootId={rootPost.id}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+RhsThread.defaultProps = {
+ fromSearch: '',
+ isMentionSearch: false
+};
+RhsThread.propTypes = {
+ fromSearch: React.PropTypes.string,
+ isMentionSearch: React.PropTypes.bool
+};
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index 8334b345b..df75e3adf 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -1,11 +1,9 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-
-var SearchResults =require('./search_results.jsx');
-var PostRight =require('./post_right.jsx');
+var SearchResults = require('./search_results.jsx');
+var RhsThread = require('./rhs_thread.jsx');
var PostStore = require('../stores/post_store.jsx');
-var Constants = require('../utils/constants.jsx');
var utils = require('../utils/utils.jsx');
function getStateFromStores(from_search) {
@@ -39,8 +37,8 @@ module.exports = React.createClass({
}
},
resize: function() {
- $(".post-list-holder-by-time").scrollTop(100000);
- $(".post-list-holder-by-time").perfectScrollbar('update');
+ var postHolder = $('.post-list-holder-by-time');
+ postHolder[0].scrollTop = postHolder[0].scrollHeight - 224;
},
getInitialState: function() {
return getStateFromStores();
@@ -72,7 +70,7 @@ module.exports = React.createClass({
content = <SearchResults isMentionSearch={this.state.is_mention_search} />;
}
else if (this.state.post_right_visible) {
- content = <PostRight fromSearch={this.state.from_search} isMentionSearch={this.state.is_mention_search} />;
+ content = <RhsThread fromSearch={this.state.from_search} isMentionSearch={this.state.is_mention_search} />;
}
return (
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 2fffb17d0..4038814d2 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -99,8 +99,52 @@ var PostStore = assign({}, EventEmitter.prototype, {
}
return null;
},
- storePosts: function storePosts(channelId, posts) {
- this.pStorePosts(channelId, posts);
+ storePosts: function storePosts(channelId, newPostList) {
+ if (isPostListNull(newPostList)) {
+ return;
+ }
+
+ var postList = makePostListNonNull(PostStore.getPosts(channelId));
+
+ for (var pid in newPostList.posts) {
+ var np = newPostList.posts[pid];
+ if (np.delete_at === 0) {
+ postList.posts[pid] = np;
+ if (postList.order.indexOf(pid) === -1) {
+ postList.order.push(pid);
+ }
+ } else {
+ if (pid in postList.posts) {
+ delete postList.posts[pid];
+ }
+
+ var index = postList.order.indexOf(pid);
+ if (index !== -1) {
+ postList.order.splice(index, 1);
+ }
+ }
+ }
+
+ postList.order.sort(function postSort(a, b) {
+ if (postList.posts[a].create_at > postList.posts[b].create_at) {
+ return -1;
+ }
+ if (postList.posts[a].create_at < postList.posts[b].create_at) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ var latestUpdate = 0;
+ for (var pid in postList.posts) {
+ if (postList.posts[pid].update_at > latestUpdate) {
+ latestUpdate = postList.posts[pid].update_at;
+ }
+ }
+
+ this.storeLatestUpdate(channelId, latestUpdate);
+ this.pStorePosts(channelId, postList);
this.emitChange();
},
pStorePosts: function pStorePosts(channelId, posts) {
@@ -115,9 +159,7 @@ var PostStore = assign({}, EventEmitter.prototype, {
},
pStorePost: function(post) {
var postList = PostStore.getPosts(post.channel_id);
- if (!postList) {
- return;
- }
+ postList = makePostListNonNull(postList);
if (post.pending_post_id !== '') {
this.removePendingPost(post.channel_id, post.pending_post_id);
@@ -132,13 +174,28 @@ var PostStore = assign({}, EventEmitter.prototype, {
this.pStorePosts(post.channel_id, postList);
},
+ removePost: function(postId, channelId) {
+ var postList = PostStore.getPosts(channelId);
+ if (isPostListNull(postList)) {
+ return;
+ }
+
+ if (postId in postList.posts) {
+ delete postList.posts[postId];
+ }
+
+ var index = postList.order.indexOf(postId);
+ if (index !== -1) {
+ postList.order.splice(index, 1);
+ }
+
+ this.pStorePosts(channelId, postList);
+ },
storePendingPost: function(post) {
post.state = Constants.POST_LOADING;
var postList = this.getPendingPosts(post.channel_id);
- if (!postList) {
- postList = {posts: {}, order: []};
- }
+ postList = makePostListNonNull(postList);
postList.posts[post.pending_post_id] = post;
postList.order.unshift(post.pending_post_id);
@@ -200,15 +257,13 @@ var PostStore = assign({}, EventEmitter.prototype, {
},
_removePendingPost: function(channelId, pendingPostId) {
var postList = this.getPendingPosts(channelId);
- if (!postList) {
- return;
- }
+ postList = makePostListNonNull(postList);
if (pendingPostId in postList.posts) {
delete postList.posts[pendingPostId];
}
var index = postList.order.indexOf(pendingPostId);
- if (index >= 0) {
+ if (index !== -1) {
postList.order.splice(index, 1);
}
@@ -221,9 +276,7 @@ var PostStore = assign({}, EventEmitter.prototype, {
},
updatePendingPost: function(post) {
var postList = this.getPendingPosts(post.channel_id);
- if (!postList) {
- postList = {posts: {}, order: []};
- }
+ postList = makePostListNonNull(postList);
if (postList.order.indexOf(post.pending_post_id) === -1) {
return;
@@ -293,6 +346,12 @@ var PostStore = assign({}, EventEmitter.prototype, {
BrowserStore.setItem(key, value);
}
});
+ },
+ storeLatestUpdate: function(channelId, time) {
+ BrowserStore.setItem('latest_post_' + channelId, time);
+ },
+ getLatestUpdate: function(channelId) {
+ return BrowserStore.getItem('latest_post_' + channelId, 0);
}
});
@@ -301,8 +360,7 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
switch (action.type) {
case ActionTypes.RECIEVED_POSTS:
- PostStore.pStorePosts(action.id, action.post_list);
- PostStore.emitChange();
+ PostStore.storePosts(action.id, makePostListNonNull(action.post_list));
break;
case ActionTypes.RECIEVED_POST:
PostStore.pStorePost(action.post);
@@ -331,3 +389,36 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
});
module.exports = PostStore;
+
+function makePostListNonNull(pl) {
+ var postList = pl;
+ if (postList == null) {
+ postList = {order: [], posts: {}};
+ }
+
+ if (postList.order == null) {
+ postList.order = [];
+ }
+
+ if (postList.posts == null) {
+ postList.posts = {};
+ }
+
+ return postList;
+}
+
+function isPostListNull(pl) {
+ if (pl == null) {
+ return true;
+ }
+
+ if (pl.posts == null) {
+ return true;
+ }
+
+ if (pl.order == null) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 349fe9021..4b0b90dc7 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -344,14 +344,14 @@ module.exports.search = function(terms) {
);
}
-module.exports.getPosts = function(force, id, maxPosts) {
+module.exports.getPostsPage = function(force, id, maxPosts) {
if (PostStore.getCurrentPosts() == null || force) {
var channelId = id;
if (channelId == null) {
channelId = ChannelStore.getCurrentId();
}
- if (isCallInProgress('getPosts_' + channelId)) {
+ if (isCallInProgress('getPostsPage_' + channelId)) {
return;
}
@@ -371,9 +371,9 @@ module.exports.getPosts = function(force, id, maxPosts) {
}
if (channelId != null) {
- callTracker['getPosts_' + channelId] = utils.getTimestamp();
+ callTracker['getPostsPage_' + channelId] = utils.getTimestamp();
- client.getPosts(
+ client.getPostsPage(
channelId,
0,
numPosts,
@@ -389,15 +389,58 @@ module.exports.getPosts = function(force, id, maxPosts) {
module.exports.getProfiles();
},
function(err) {
- dispatchError(err, 'getPosts');
+ dispatchError(err, 'getPostsPage');
},
function() {
- callTracker['getPosts_' + channelId] = 0;
+ callTracker['getPostsPage_' + channelId] = 0;
}
);
}
}
+};
+
+function getPosts(id) {
+ var channelId = id;
+ if (channelId == null) {
+ if (ChannelStore.getCurrentId() == null) {
+ return;
+ }
+ channelId = ChannelStore.getCurrentId();
+ }
+
+ if (isCallInProgress('getPosts_' + channelId)) {
+ return;
+ }
+
+ var latestUpdate = PostStore.getLatestUpdate(channelId);
+
+ callTracker['getPosts_' + channelId] = utils.getTimestamp();
+
+ client.getPosts(
+ channelId,
+ latestUpdate,
+ function success(data, textStatus, xhr) {
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POSTS,
+ id: channelId,
+ post_list: data
+ });
+
+ module.exports.getProfiles();
+ },
+ function fail(err) {
+ dispatchError(err, 'getPosts');
+ },
+ function complete() {
+ callTracker['getPosts_' + channelId] = 0;
+ }
+ );
}
+module.exports.getPosts = getPosts;
function getMe() {
if (isCallInProgress('getMe')) {
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 13d6c3f54..70220c71e 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -653,7 +653,7 @@ module.exports.executeCommand = function(channelId, command, suggest, success, e
});
};
-module.exports.getPosts = function(channelId, offset, limit, success, error, complete) {
+module.exports.getPostsPage = function(channelId, offset, limit, success, error, complete) {
$.ajax({
cache: false,
url: '/api/v1/channels/' + channelId + '/posts/' + offset + '/' + limit,
@@ -669,6 +669,21 @@ module.exports.getPosts = function(channelId, offset, limit, success, error, com
});
};
+module.exports.getPosts = function(channelId, since, success, error, complete) {
+ $.ajax({
+ url: '/api/v1/channels/' + channelId + '/posts/' + since,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: true,
+ success: success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPosts', xhr, status, err);
+ error(e);
+ },
+ complete: complete
+ });
+};
+
module.exports.getPost = function(channelId, postId, success, error) {
$.ajax({
cache: false,
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index f19dd2b47..13989ad82 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -765,7 +765,7 @@ function switchChannel(channel, teammateName) {
AsyncClient.getChannels(true, true, true);
AsyncClient.getChannelExtraInfo(true);
- AsyncClient.getPosts(true, channel.id, Constants.POST_CHUNK_SIZE);
+ AsyncClient.getPosts(channel.id);
$('.inner__wrap').removeClass('move--right');
$('.sidebar--left').removeClass('move--right');
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index e665be6b9..0605e9c3b 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -139,6 +139,9 @@ body.ios {
width: 100%;
padding: 1em 0 0;
position: relative;
+ &.hide-scroll::-webkit-scrollbar {
+ width: 0px !important;
+ }
}
.post-list__table {
display: table;