Refactored post handling/updating on both the client and server.

Этот коммит содержится в:
JoramWilander
2015-08-26 12:09:01 -04:00
родитель 8356e25f80
Коммит 041d89b85a
28 изменённых файлов: 1643 добавлений и 902 удалений

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

@@ -391,6 +391,8 @@ func JoinChannel(c *Context, channelId string, role string) {
c.Err = model.NewAppError("joinChannel", "Failed to send join request", "") c.Err = model.NewAppError("joinChannel", "Failed to send join request", "")
return return
} }
UpdateChannelAccessCacheAndForget(c.Session.TeamId, c.Session.UserId, channel.Id)
} else { } else {
c.Err = model.NewAppError("joinChannel", "You do not have the appropriate permissions", "") c.Err = model.NewAppError("joinChannel", "You do not have the appropriate permissions", "")
c.Err.StatusCode = http.StatusForbidden c.Err.StatusCode = http.StatusForbidden

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

@@ -28,6 +28,7 @@ func InitPost(r *mux.Router) {
sr.Handle("/valet_create", ApiUserRequired(createValetPost)).Methods("POST") sr.Handle("/valet_create", ApiUserRequired(createValetPost)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updatePost)).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/{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]+}", ApiUserRequired(getPost)).Methods("GET")
sr.Handle("/post/{post_id:[A-Za-z0-9]+}/delete", ApiUserRequired(deletePost)).Methods("POST") 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) rpost := result.Data.(*model.Post)
message := model.NewMessage(c.Session.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED) message := model.NewMessage(c.Session.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED)
message.Add("post_id", rpost.Id) message.Add("post", rpost.ToJson())
message.Add("channel_id", rpost.ChannelId)
message.Add("message", rpost.Message)
PublishAndForget(message) 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) { func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r) params := mux.Vars(r)

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

@@ -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) { func TestSearchPosts(t *testing.T) {
Setup() Setup()

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

@@ -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 { func hasPermissionsToChannel(sc store.StoreChannel) bool {
if cresult := <-sc; cresult.Err != nil { if cresult := <-sc; cresult.Err != nil {
return false return false

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

@@ -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) { func (h *Hub) Register(webConn *WebConn) {
h.register <- webConn h.register <- webConn
} }

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

@@ -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
}
}
}

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

@@ -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) { 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 { if r, err := c.DoGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil {
return nil, err return nil, err

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

@@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"github.com/mattermost/platform/model" "github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils" "github.com/mattermost/platform/utils"
"strconv"
"strings" "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()) 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.AddPost(&post)
pl.AddOrder(id) 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))} list := &model.PostList{Order: make([]string, 0, len(posts))}
for _, p := range 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.AddPost(p)
list.AddOrder(p.Id) list.AddOrder(p.Id)
} }
for _, p := range parents { 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) list.AddPost(p)
} }
@@ -299,6 +276,62 @@ func (s SqlPostStore) GetPosts(channelId string, offset int, limit int) StoreCha
return storeChannel 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 { func (s SqlPostStore) getRootPosts(channelId string, offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel) storeChannel := make(StoreChannel)

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

@@ -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) { func TestPostStoreSearch(t *testing.T) {
Setup() Setup()

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

@@ -75,6 +75,7 @@ type PostStore interface {
Get(id string) StoreChannel Get(id string) StoreChannel
Delete(postId string, time int64) StoreChannel Delete(postId string, time int64) StoreChannel
GetPosts(channelId string, offset int, limit int) StoreChannel GetPosts(channelId string, offset int, limit int) StoreChannel
GetPostsSince(channelId string, time int64) StoreChannel
GetEtag(channelId string) StoreChannel GetEtag(channelId string) StoreChannel
Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel
} }

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

@@ -5,63 +5,83 @@
to the server on page load. This is to prevent other React controls from spamming to the server on page load. This is to prevent other React controls from spamming
AsyncClient with requests. */ AsyncClient with requests. */
var BrowserStore = require('../stores/browser_store.jsx');
var AsyncClient = require('../utils/async_client.jsx'); var AsyncClient = require('../utils/async_client.jsx');
var SocketStore = require('../stores/socket_store.jsx'); var SocketStore = require('../stores/socket_store.jsx');
var ChannelStore = require('../stores/channel_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx');
var PostStore = require('../stores/post_store.jsx'); var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var Constants = require('../utils/constants.jsx'); var Constants = require('../utils/constants.jsx');
var utils = require('../utils/utils.jsx');
module.exports = React.createClass({ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
/* Start initial aysnc loads */ /* Initial aysnc loads */
AsyncClient.getMe(); AsyncClient.getMe();
AsyncClient.getPosts(true, ChannelStore.getCurrentId(), Constants.POST_CHUNK_SIZE); AsyncClient.getPosts(ChannelStore.getCurrentId());
AsyncClient.getChannels(true, true); AsyncClient.getChannels(true, true);
AsyncClient.getChannelExtraInfo(true); AsyncClient.getChannelExtraInfo(true);
AsyncClient.findTeams(); AsyncClient.findTeams();
AsyncClient.getStatuses(); AsyncClient.getStatuses();
AsyncClient.getMyTeam(); AsyncClient.getMyTeam();
/* End of async loads */
/* Perform pending post clean-up */ /* Perform pending post clean-up */
PostStore.clearPendingPosts(); PostStore.clearPendingPosts();
/* End pending post clean-up */
/* Start interval functions */ /* Set up interval functions */
setInterval( setInterval(
function pollStatuses() { function pollStatuses() {
AsyncClient.getStatuses(); AsyncClient.getStatuses();
}, 30000); }, 30000);
/* End interval functions */
/* Start device tracking setup */ /* Device tracking setup */
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent); var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
if (iOS) { if (iOS) {
$('body').addClass('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.isActive = true;
$(window).focus(function() { $(window).focus(function windowFocus() {
AsyncClient.updateLastViewedAt(); AsyncClient.updateLastViewedAt();
window.isActive = true; window.isActive = true;
}); });
$(window).blur(function() { $(window).blur(function windowBlur() {
window.isActive = false; window.isActive = false;
}); });
/* End window active tracking setup */
/* Start global change listeners setup */ /* Start global change listeners setup */
SocketStore.addChangeListener(this._onSocketChange); SocketStore.addChangeListener(this.onSocketChange);
/* End global change listeners setup */
/* 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) { onSocketChange: function(msg) {
if (msg && msg.user_id) { if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
UserStore.setStatus(msg.user_id, 'online'); UserStore.setStatus(msg.user_id, 'online');
} }
}, },

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

@@ -29,8 +29,6 @@ module.exports = React.createClass({
return; return;
} }
this.setState({submitting: true, serverError: null});
var post = {}; var post = {};
post.filenames = []; post.filenames = [];
post.message = this.state.messageText; post.message = this.state.messageText;
@@ -57,11 +55,10 @@ module.exports = React.createClass({
PostStore.storePendingPost(post); PostStore.storePendingPost(post);
PostStore.storeCommentDraft(this.props.rootId, null); PostStore.storeCommentDraft(this.props.rootId, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
client.createPost(post, ChannelStore.getCurrent(), client.createPost(post, ChannelStore.getCurrent(),
function(data) { function(data) {
AsyncClient.getPosts(true, this.props.channelId); AsyncClient.getPosts(this.props.channelId);
var channel = ChannelStore.get(this.props.channelId); var channel = ChannelStore.get(this.props.channelId);
var member = ChannelStore.getMember(this.props.channelId); var member = ChannelStore.getMember(this.props.channelId);
@@ -91,6 +88,8 @@ module.exports = React.createClass({
this.setState(state); this.setState(state);
}.bind(this) }.bind(this)
); );
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
}, },
commentMsgKeyPress: function(e) { commentMsgKeyPress: function(e) {
if (e.which === 13 && !e.shiftKey && !e.altKey) { if (e.which === 13 && !e.shiftKey && !e.altKey) {

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

@@ -82,7 +82,7 @@ module.exports = React.createClass({
client.createPost(post, channel, client.createPost(post, channel,
function(data) { function(data) {
this.resizePostHolder(); this.resizePostHolder();
AsyncClient.getPosts(true); AsyncClient.getPosts();
var member = ChannelStore.getMember(channel.id); var member = ChannelStore.getMember(channel.id);
member.msg_count = channel.total_msg_count; member.msg_count = channel.total_msg_count;
@@ -112,8 +112,6 @@ module.exports = React.createClass({
}.bind(this) }.bind(this)
); );
} }
$('.post-list-holder-by-time').perfectScrollbar('update');
}, },
componentDidUpdate: function() { componentDidUpdate: function() {
this.resizePostHolder(); this.resizePostHolder();

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

@@ -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), }.bind(this),
function(err) { function(err) {
AsyncClient.dispatchError(err, "deletePost"); AsyncClient.dispatchError(err, "deletePost");

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

@@ -25,7 +25,7 @@ module.exports = React.createClass({
Client.updatePost(updatedPost, Client.updatePost(updatedPost,
function(data) { function(data) {
AsyncClient.getPosts(true, this.state.channel_id); AsyncClient.getPosts(this.state.channel_id);
window.scrollTo(0, 0); window.scrollTo(0, 0);
}.bind(this), }.bind(this),
function(err) { function(err) {

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

@@ -11,11 +11,12 @@ var ChannelStore = require('../stores/channel_store.jsx');
var client = require('../utils/client.jsx'); var client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx'); var AsyncClient = require('../utils/async_client.jsx');
var ActionTypes = Constants.ActionTypes; var ActionTypes = Constants.ActionTypes;
var utils = require('../utils/utils.jsx');
var PostInfo = require('./post_info.jsx'); var PostInfo = require('./post_info.jsx');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: "Post", displayName: 'Post',
handleCommentClick: function(e) { handleCommentClick: function(e) {
e.preventDefault(); e.preventDefault();
@@ -43,7 +44,7 @@ module.exports = React.createClass({
var post = this.props.post; var post = this.props.post;
client.createPost(post, post.channel_id, client.createPost(post, post.channel_id,
function(data) { function(data) {
AsyncClient.getPosts(true); AsyncClient.getPosts();
var channel = ChannelStore.get(post.channel_id); var channel = ChannelStore.get(post.channel_id);
var member = ChannelStore.getMember(post.channel_id); var member = ChannelStore.getMember(post.channel_id);
@@ -67,6 +68,13 @@ module.exports = React.createClass({
PostStore.updatePendingPost(post); PostStore.updatePendingPost(post);
this.forceUpdate(); this.forceUpdate();
}, },
shouldComponentUpdate: function(nextProps) {
if (!utils.areStatesEqual(nextProps.post, this.props.post)) {
return true;
}
return false;
},
getInitialState: function() { getInitialState: function() {
return { }; 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 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 = ""; var postType = '';
if (type != "Post"){ if (type != 'Post'){
postType = "post--comment"; postType = 'post--comment';
} }
var currentUserCss = ""; var currentUserCss = '';
if (UserStore.getCurrentId() === post.user_id) { if (UserStore.getCurrentId() === post.user_id) {
currentUserCss = "current--user"; currentUserCss = 'current--user';
} }
var userProfile = UserStore.getProfile(post.user_id); var userProfile = UserStore.getProfile(post.user_id);
@@ -109,18 +117,23 @@ module.exports = React.createClass({
timestamp = userProfile.update_at; timestamp = userProfile.update_at;
} }
var sameUserClass = '';
if (this.props.sameUser) {
sameUserClass = 'same--user';
}
return ( return (
<div> <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 ? { !this.props.hideProfilePic ?
<div className="post-profile-img__container"> <div className='post-profile-img__container'>
<img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image?time=" + timestamp} height="36" width="36" /> <img className='post-profile-img' src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} height='36' width='36' />
</div> </div>
: null } : null }
<div className="post__content"> <div className='post__content'>
<PostHeader ref="header" post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} isLastComment={this.props.isLastComment} /> <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} /> <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> </div>
</div> </div>

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

@@ -15,124 +15,116 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Constants = require('../utils/constants.jsx'); var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes; var ActionTypes = Constants.ActionTypes;
function getStateFromStores() { export default class PostList extends React.Component {
var channel = ChannelStore.getCurrent(); constructor() {
super();
if (channel == null) { this.gotMorePosts = false;
channel = {}; 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(); if (channel == null) {
var deletedPosts = PostStore.getUnseenDeletedPosts(channel.id); channel = {};
if (deletedPosts && Object.keys(deletedPosts).length > 0) {
for (var pid in deletedPosts) {
postList.posts[pid] = deletedPosts[pid];
postList.order.unshift(pid);
} }
postList.order.sort(function postSort(a, b) { var postList = PostStore.getCurrentPosts();
if (postList.posts[a].create_at > postList.posts[b].create_at) {
return -1; if (postList != null) {
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);
}
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;
});
} }
if (postList.posts[a].create_at < postList.posts[b].create_at) {
return 1; var pendingPostList = PostStore.getPendingPosts(channel.id);
if (pendingPostList) {
postList.order = pendingPostList.order.concat(postList.order);
for (var ppid in pendingPostList.posts) {
postList.posts[ppid] = pendingPostList.posts[ppid];
}
} }
return 0; }
});
var lastViewed = Number.MAX_VALUE;
if (ChannelStore.getCurrentMember() != null) {
lastViewed = ChannelStore.getCurrentMember().last_viewed_at;
}
return {
postList: postList,
channel: channel,
lastViewed: lastViewed
};
} }
componentDidMount() {
var pendingPostList = PostStore.getPendingPosts(channel.id);
if (pendingPostList) {
postList.order = pendingPostList.order.concat(postList.order);
for (var ppid in pendingPostList.posts) {
postList.posts[ppid] = pendingPostList.posts[ppid];
}
}
return {
postList: postList,
channel: channel
};
}
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 (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');
}
PostStore.addChangeListener(this.onChange); PostStore.addChangeListener(this.onChange);
ChannelStore.addChangeListener(this.onChange); ChannelStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onTimeChange); UserStore.addStatusesChangeListener(this.onTimeChange);
SocketStore.addChangeListener(this.onSocketChange); SocketStore.addChangeListener(this.onSocketChange);
$('.post-list-holder-by-time').perfectScrollbar(); var postHolder = $('.post-list-holder-by-time');
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;
$('.modal').on('show.bs.modal', function onShow() { $('.modal').on('show.bs.modal', function onShow() {
$('.modal-body').css('overflow-y', 'auto'); $('.modal-body').css('overflow-y', 'auto');
$('.modal-body').css('max-height', $(window).height() * 0.7); $('.modal-body').css('max-height', $(window).height() * 0.7);
}); });
var self = this;
$(window).resize(function resize() { $(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 (self.scrollPosition >= postHolder.scrollHeight || (self.oldScrollHeight !== postHolder.scrollHeight && self.scrollPosition >= self.oldScrollHeight) || self.oldZoom !== newZoom) {
self.resize();
}
self.oldZoom = newZoom;
if ($('#create_post').length > 0) { if ($('#create_post').length > 0) {
var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
$('.post-list-holder-by-time').css('height', height + 'px'); postHolder.css('height', height + 'px');
} }
});
$(postHolder).scroll(function scroll() { if (!this.scrolled) {
if (!self.preventScrollTrigger) { this.scrollToBottom();
self.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight();
} }
self.preventScrollTrigger = false; }.bind(this));
});
postHolder.scroll(function scroll() {
var position = postHolder.scrollTop() + postHolder.height() + 14;
var bottom = postHolder[0].scrollHeight;
if (position >= bottom) {
this.scrolled = false;
} else {
this.scrolled = true;
}
if (this.isUserScroll) {
this.userHasSeenNew = true;
}
this.isUserScroll = true;
}.bind(this));
$('body').on('click.userpopover', function popOver(e) { $('body').on('click.userpopover', function popOver(e) {
if ($(e.target).attr('data-toggle') !== 'popover' && 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'); $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
} }
}); });
},
componentDidUpdate: function() { this.scrollToBottom();
this.resize(); setTimeout(this.scrollToBottom, 100);
var postHolder = $('.post-list-holder-by-time')[0]; }
this.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight(); componentDidUpdate(prevProps, prevState) {
this.oldScrollHeight = postHolder.scrollHeight;
$('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div .post').removeClass('post--last');
$('.post-list__content div:last-child .post').addClass('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); PostStore.removeChangeListener(this.onChange);
ChannelStore.removeChangeListener(this.onChange); ChannelStore.removeChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onTimeChange); UserStore.removeStatusesChangeListener(this.onTimeChange);
SocketStore.removeChangeListener(this.onSocketChange); SocketStore.removeChangeListener(this.onSocketChange);
$('body').off('click.userpopover'); $('body').off('click.userpopover');
$('.modal').off('show.bs.modal'); $('.modal').off('show.bs.modal');
}, }
resize: function() { scrollTo(val) {
var postHolder = $('.post-list-holder-by-time')[0]; this.isUserScroll = false;
this.preventScrollTrigger = true; var postHolder = $('.post-list-holder-by-time');
if (this.gotMorePosts) { postHolder[0].scrollTop = val;
this.gotMorePosts = false; }
$(postHolder).scrollTop($(postHolder).scrollTop() + (postHolder.scrollHeight - this.oldScrollHeight)); scrollToBottom(force) {
} else if ($('#new_message')[0] && !this.scrolledToNew) { this.isUserScroll = false;
$(postHolder).scrollTop($(postHolder).scrollTop() + $('#new_message').offset().top - 63); var postHolder = $('.post-list-holder-by-time');
this.scrolledToNew = true; if ($('#new_message')[0] && !this.userHasSeenNew && !force) {
$('#new_message')[0].scrollIntoView();
} else { } else {
$(postHolder).scrollTop(postHolder.scrollHeight); postHolder.addClass('hide-scroll');
postHolder[0].scrollTop = postHolder[0].scrollHeight;
postHolder.removeClass('hide-scroll');
} }
$(postHolder).perfectScrollbar('update'); }
}, onChange() {
onChange: function() { var newState = this.getStateFromStores();
var newState = getStateFromStores();
if (!utils.areStatesEqual(newState, this.state)) { 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) { if (this.state.channel.id !== newState.channel.id) {
PostStore.clearUnseenDeletedPosts(this.state.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); this.setState(newState);
} }
}, }
onSocketChange: function(msg) { onSocketChange(msg) {
var postList; var postList;
var post; var post;
if (msg.action === 'posted') { if (msg.action === 'posted' || msg.action === 'post_edited') {
post = JSON.parse(msg.props.post); post = JSON.parse(msg.props.post);
PostStore.storePost(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') { } else if (msg.action === 'post_deleted') {
var activeRoot = $(document.activeElement).closest('.comment-create-body')[0]; var activeRoot = $(document.activeElement).closest('.comment-create-body')[0];
var activeRootPostId = ''; var activeRootPostId = '';
@@ -244,16 +261,8 @@ module.exports = React.createClass({
postList = this.state.postList; postList = this.state.postList;
PostStore.storeUnseenDeletedPost(post); PostStore.storeUnseenDeletedPost(post);
PostStore.removePost(post, true);
if (postList.posts[post.id]) { PostStore.emitChange();
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);
}
if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) { if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) {
$('#post_deleted').modal('show'); $('#post_deleted').modal('show');
@@ -261,8 +270,8 @@ module.exports = React.createClass({
} else if (msg.action === 'new_user') { } else if (msg.action === 'new_user') {
AsyncClient.getProfiles(); AsyncClient.getProfiles();
} }
}, }
onTimeChange: function() { onTimeChange() {
if (!this.state.postList) { if (!this.state.postList) {
return; return;
} }
@@ -273,11 +282,256 @@ module.exports = React.createClass({
} }
this.refs[id].forceUpdateInfo(); this.refs[id].forceUpdateInfo();
} }
}, }
getMorePosts: function(e) { createDMIntroMessage(channel) {
e.preventDefault(); var teammate = utils.getDirectTeammate(channel.id);
if (!this.state.postList) { if (teammate) {
var teammateName = teammate.username;
if (teammate.nickname.length > 0) {
teammateName = teammate.nickname;
}
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>
);
}
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;
}
}
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.';
}
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 numToDisplay = this.state.numToDisplay;
if (order.length - 1 < numToDisplay) {
numToDisplay = order.length - 1;
}
for (var i = numToDisplay; i >= 0; i--) {
var post = posts[order[i]];
var parentPost = posts[post.parent_id];
var sameUser = false;
var sameRoot = false;
var hideProfilePic = false;
var prevPost = posts[order[i + 1]];
if (prevPost) {
sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
// 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);
}
// 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}
/>
);
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>
);
}
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;
}
return postCtls;
}
loadMorePosts() {
if (this.state.postList == null) {
return; return;
} }
@@ -287,257 +541,72 @@ module.exports = React.createClass({
$(this.refs.loadmore.getDOMNode()).text('Retrieving more messages...'); $(this.refs.loadmore.getDOMNode()).text('Retrieving more messages...');
var self = this; Client.getPostsPage(
var currentPos = $('.post-list').scrollTop; 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});
Client.getPosts( if (!data) {
channelId, return;
order.length,
Constants.POST_CHUNK_SIZE,
function success(data) {
$(self.refs.loadmore.getDOMNode()).text('Load more messages');
if (!data) {
return;
}
if (data.order.length === 0) {
return;
}
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');
} }
);
}, if (data.order.length === 0) {
getInitialState: function() { return;
return getStateFromStores(); }
},
render: function() { 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 order = [];
var posts; var posts;
var channel = this.state.channel;
var lastViewed = Number.MAX_VALUE;
if (ChannelStore.getCurrentMember() != null) {
lastViewed = ChannelStore.getCurrentMember().last_viewed_at;
}
if (this.state.postList != null) { if (this.state.postList != null) {
posts = this.state.postList.posts; posts = this.state.postList.posts;
order = this.state.postList.order; order = this.state.postList.order;
} }
var renderedLastViewed = false;
var userId = '';
if (UserStore.getCurrentId()) {
userId = UserStore.getCurrentId();
} else {
return <div/>;
}
var channel = this.state.channel;
var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>; var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>;
var userStyle = {color: UserStore.getCurrentUser().props.theme};
if (channel != null) { if (channel != null) {
if (order.length > 0 && order.length % Constants.POST_CHUNK_SIZE === 0) { if (order.length > this.state.numToDisplay) {
moreMessages = <a ref='loadmore' className='more-messages-text theme' href='#' onClick={this.getMorePosts}>Load more messages</a>; moreMessages = (
} else if (channel.type === 'D') { <a
var teammate = utils.getDirectTeammate(channel.id); ref='loadmore'
className='more-messages-text theme'
if (teammate) { href='#'
var teammateName = teammate.username; onClick={this.loadMorePosts}
if (teammate.nickname.length > 0) { >
teammateName = teammate.nickname; Load more messages
} </a>
);
moreMessages = ( } else {
<div className='channel-intro'> moreMessages = this.createChannelIntroMessage(channel);
<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;
}
}
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;
}
}
}
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.';
}
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) + '.';
}
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>
);
}
} }
} }
var postCtls = []; var postCtls = [];
if (posts) { if (posts) {
var previousPostDay = new Date(0); postCtls = this.createPosts(posts, order);
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 sameUser = '';
var sameRoot = false;
var hideProfilePic = false;
var prevPost;
if (i < order.length - 1) {
prevPost = posts[order[i + 1]];
}
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);
// 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);
}
// 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 ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id}
posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment}
/>
);
currentPostDay = utils.getDateForUnixTicks(post.create_at);
if (currentPostDay.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 (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;
}
} else { } else {
postCtls.push(<LoadingScreen position='absolute' />); postCtls.push(<LoadingScreen position='absolute' />);
} }
@@ -553,4 +622,4 @@ module.exports = React.createClass({
</div> </div>
); );
} }
}); }

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

@@ -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>
);
}
});

207
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
};

81
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
};

145
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
};

215
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
};

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

@@ -1,11 +1,9 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var SearchResults = require('./search_results.jsx');
var SearchResults =require('./search_results.jsx'); var RhsThread = require('./rhs_thread.jsx');
var PostRight =require('./post_right.jsx');
var PostStore = require('../stores/post_store.jsx'); var PostStore = require('../stores/post_store.jsx');
var Constants = require('../utils/constants.jsx');
var utils = require('../utils/utils.jsx'); var utils = require('../utils/utils.jsx');
function getStateFromStores(from_search) { function getStateFromStores(from_search) {
@@ -39,8 +37,8 @@ module.exports = React.createClass({
} }
}, },
resize: function() { resize: function() {
$(".post-list-holder-by-time").scrollTop(100000); var postHolder = $('.post-list-holder-by-time');
$(".post-list-holder-by-time").perfectScrollbar('update'); postHolder[0].scrollTop = postHolder[0].scrollHeight - 224;
}, },
getInitialState: function() { getInitialState: function() {
return getStateFromStores(); return getStateFromStores();
@@ -72,7 +70,7 @@ module.exports = React.createClass({
content = <SearchResults isMentionSearch={this.state.is_mention_search} />; content = <SearchResults isMentionSearch={this.state.is_mention_search} />;
} }
else if (this.state.post_right_visible) { 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 ( return (

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

@@ -99,8 +99,52 @@ var PostStore = assign({}, EventEmitter.prototype, {
} }
return null; return null;
}, },
storePosts: function storePosts(channelId, posts) { storePosts: function storePosts(channelId, newPostList) {
this.pStorePosts(channelId, posts); 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(); this.emitChange();
}, },
pStorePosts: function pStorePosts(channelId, posts) { pStorePosts: function pStorePosts(channelId, posts) {
@@ -115,9 +159,7 @@ var PostStore = assign({}, EventEmitter.prototype, {
}, },
pStorePost: function(post) { pStorePost: function(post) {
var postList = PostStore.getPosts(post.channel_id); var postList = PostStore.getPosts(post.channel_id);
if (!postList) { postList = makePostListNonNull(postList);
return;
}
if (post.pending_post_id !== '') { if (post.pending_post_id !== '') {
this.removePendingPost(post.channel_id, 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); 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) { storePendingPost: function(post) {
post.state = Constants.POST_LOADING; post.state = Constants.POST_LOADING;
var postList = this.getPendingPosts(post.channel_id); var postList = this.getPendingPosts(post.channel_id);
if (!postList) { postList = makePostListNonNull(postList);
postList = {posts: {}, order: []};
}
postList.posts[post.pending_post_id] = post; postList.posts[post.pending_post_id] = post;
postList.order.unshift(post.pending_post_id); postList.order.unshift(post.pending_post_id);
@@ -200,15 +257,13 @@ var PostStore = assign({}, EventEmitter.prototype, {
}, },
_removePendingPost: function(channelId, pendingPostId) { _removePendingPost: function(channelId, pendingPostId) {
var postList = this.getPendingPosts(channelId); var postList = this.getPendingPosts(channelId);
if (!postList) { postList = makePostListNonNull(postList);
return;
}
if (pendingPostId in postList.posts) { if (pendingPostId in postList.posts) {
delete postList.posts[pendingPostId]; delete postList.posts[pendingPostId];
} }
var index = postList.order.indexOf(pendingPostId); var index = postList.order.indexOf(pendingPostId);
if (index >= 0) { if (index !== -1) {
postList.order.splice(index, 1); postList.order.splice(index, 1);
} }
@@ -221,9 +276,7 @@ var PostStore = assign({}, EventEmitter.prototype, {
}, },
updatePendingPost: function(post) { updatePendingPost: function(post) {
var postList = this.getPendingPosts(post.channel_id); var postList = this.getPendingPosts(post.channel_id);
if (!postList) { postList = makePostListNonNull(postList);
postList = {posts: {}, order: []};
}
if (postList.order.indexOf(post.pending_post_id) === -1) { if (postList.order.indexOf(post.pending_post_id) === -1) {
return; return;
@@ -293,6 +346,12 @@ var PostStore = assign({}, EventEmitter.prototype, {
BrowserStore.setItem(key, value); 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) { switch (action.type) {
case ActionTypes.RECIEVED_POSTS: case ActionTypes.RECIEVED_POSTS:
PostStore.pStorePosts(action.id, action.post_list); PostStore.storePosts(action.id, makePostListNonNull(action.post_list));
PostStore.emitChange();
break; break;
case ActionTypes.RECIEVED_POST: case ActionTypes.RECIEVED_POST:
PostStore.pStorePost(action.post); PostStore.pStorePost(action.post);
@@ -331,3 +389,36 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
}); });
module.exports = PostStore; 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;
}

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

@@ -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) { if (PostStore.getCurrentPosts() == null || force) {
var channelId = id; var channelId = id;
if (channelId == null) { if (channelId == null) {
channelId = ChannelStore.getCurrentId(); channelId = ChannelStore.getCurrentId();
} }
if (isCallInProgress('getPosts_' + channelId)) { if (isCallInProgress('getPostsPage_' + channelId)) {
return; return;
} }
@@ -371,9 +371,9 @@ module.exports.getPosts = function(force, id, maxPosts) {
} }
if (channelId != null) { if (channelId != null) {
callTracker['getPosts_' + channelId] = utils.getTimestamp(); callTracker['getPostsPage_' + channelId] = utils.getTimestamp();
client.getPosts( client.getPostsPage(
channelId, channelId,
0, 0,
numPosts, numPosts,
@@ -389,15 +389,58 @@ module.exports.getPosts = function(force, id, maxPosts) {
module.exports.getProfiles(); module.exports.getProfiles();
}, },
function(err) { function(err) {
dispatchError(err, 'getPosts'); dispatchError(err, 'getPostsPage');
}, },
function() { 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() { function getMe() {
if (isCallInProgress('getMe')) { if (isCallInProgress('getMe')) {

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

@@ -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({ $.ajax({
cache: false, cache: false,
url: '/api/v1/channels/' + channelId + '/posts/' + offset + '/' + limit, 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) { module.exports.getPost = function(channelId, postId, success, error) {
$.ajax({ $.ajax({
cache: false, cache: false,

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

@@ -765,7 +765,7 @@ function switchChannel(channel, teammateName) {
AsyncClient.getChannels(true, true, true); AsyncClient.getChannels(true, true, true);
AsyncClient.getChannelExtraInfo(true); AsyncClient.getChannelExtraInfo(true);
AsyncClient.getPosts(true, channel.id, Constants.POST_CHUNK_SIZE); AsyncClient.getPosts(channel.id);
$('.inner__wrap').removeClass('move--right'); $('.inner__wrap').removeClass('move--right');
$('.sidebar--left').removeClass('move--right'); $('.sidebar--left').removeClass('move--right');

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

@@ -139,6 +139,9 @@ body.ios {
width: 100%; width: 100%;
padding: 1em 0 0; padding: 1em 0 0;
position: relative; position: relative;
&.hide-scroll::-webkit-scrollbar {
width: 0px !important;
}
} }
.post-list__table { .post-list__table {
display: table; display: table;