From 7d4a9ad376cf346826ab13b7c0d4eec60eb5cb7d Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 7 Dec 2015 18:13:14 -0500 Subject: Added preference_changed web socket event --- api/web_team_hub.go | 4 +++- model/message.go | 17 +++++++++-------- web/react/dispatcher/event_helpers.jsx | 7 +++++++ web/react/stores/preference_store.jsx | 12 +++++++++--- web/react/stores/socket_store.jsx | 9 +++++++++ web/react/utils/constants.jsx | 4 +++- 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/api/web_team_hub.go b/api/web_team_hub.go index 6a25b7d3d..2c2386317 100644 --- a/api/web_team_hub.go +++ b/api/web_team_hub.go @@ -95,9 +95,11 @@ func ShouldSendEvent(webCon *WebConn, msg *model.Message) bool { return false } } else { - // Don't share a user's view events with other users + // Don't share a user's view or preference events with other users if msg.Action == model.ACTION_CHANNEL_VIEWED { return false + } else if msg.Action == model.ACTION_PREFERENCE_CHANGED { + return false } // Only report events to a user who is the subject of the event, or is in the channel of the event diff --git a/model/message.go b/model/message.go index 2725353ac..1cb350bbf 100644 --- a/model/message.go +++ b/model/message.go @@ -9,14 +9,15 @@ import ( ) const ( - ACTION_TYPING = "typing" - ACTION_POSTED = "posted" - ACTION_POST_EDITED = "post_edited" - ACTION_POST_DELETED = "post_deleted" - ACTION_CHANNEL_VIEWED = "channel_viewed" - ACTION_NEW_USER = "new_user" - ACTION_USER_ADDED = "user_added" - ACTION_USER_REMOVED = "user_removed" + ACTION_TYPING = "typing" + ACTION_POSTED = "posted" + ACTION_POST_EDITED = "post_edited" + ACTION_POST_DELETED = "post_deleted" + ACTION_CHANNEL_VIEWED = "channel_viewed" + ACTION_NEW_USER = "new_user" + ACTION_USER_ADDED = "user_added" + ACTION_USER_REMOVED = "user_removed" + ACTION_PREFERENCE_CHANGED = "preference_changed" ) type Message struct { diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx index 306c59e8b..297555f38 100644 --- a/web/react/dispatcher/event_helpers.jsx +++ b/web/react/dispatcher/event_helpers.jsx @@ -148,3 +148,10 @@ export function emitClearSuggestions(suggestionId) { id: suggestionId }); } + +export function emitPreferenceChangedEvent(preference) { + AppDispatcher.handleServerAction({ + type: Constants.ActionTypes.RECIEVED_PREFERENCE, + preference + }); +} diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx index 068bc29c2..e6a1d8a2b 100644 --- a/web/react/stores/preference_store.jsx +++ b/web/react/stores/preference_store.jsx @@ -90,8 +90,8 @@ class PreferenceStoreClass extends EventEmitter { return preference; } - emitChange(preferences) { - this.emit(CHANGE_EVENT, preferences); + emitChange() { + this.emit(CHANGE_EVENT); } addChangeListener(callback) { @@ -106,6 +106,12 @@ class PreferenceStoreClass extends EventEmitter { const action = payload.action; switch (action.type) { + case ActionTypes.RECIEVED_PREFERENCE: { + const preference = action.preference; + this.setPreference(preference.category, preference.name, preference.value); + this.emitChange(); + break; + } case ActionTypes.RECIEVED_PREFERENCES: { const preferences = this.getAllPreferences(); @@ -114,7 +120,7 @@ class PreferenceStoreClass extends EventEmitter { } this.setAllPreferences(preferences); - this.emitChange(preferences); + this.emitChange(); break; } } diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 29aa32a08..ee501c149 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -135,6 +135,10 @@ class SocketStoreClass extends EventEmitter { handleChannelViewedEvent(msg); break; + case SocketEvents.PREFERENCE_CHANGED: + handlePreferenceChangedEvent(msg); + break; + default: } } @@ -279,6 +283,11 @@ function handleChannelViewedEvent(msg) { } } +function handlePreferenceChangedEvent(msg) { + const preference = JSON.parse(msg.props.preference); + EventHelpers.emitPreferenceChangedEvent(preference); +} + var SocketStore = new SocketStoreClass(); /*SocketStore.dispatchToken = AppDispatcher.register((payload) => { diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 170a16049..27cf2b175 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -35,6 +35,7 @@ export default { RECIEVED_AUDITS: null, RECIEVED_TEAMS: null, RECIEVED_STATUSES: null, + RECIEVED_PREFERENCE: null, RECIEVED_PREFERENCES: null, RECIEVED_MSG: null, @@ -74,7 +75,8 @@ export default { NEW_USER: 'new_user', USER_ADDED: 'user_added', USER_REMOVED: 'user_removed', - TYPING: 'typing' + TYPING: 'typing', + PREFERENCE_CHANGED: 'preference_changed' }, //SPECIAL_MENTIONS: ['all', 'channel'], -- cgit v1.2.3-1-g7c22 From 4b51490a3836144e6407b71cbd00aa094985168a Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 7 Dec 2015 18:14:07 -0500 Subject: Moved logic for making direct channels visible to both members on post to the server --- api/post.go | 59 ++++++++++++++++++++++++++++++++++++++++ api/post_test.go | 48 ++++++++++++++++++++++++++++++++ web/react/components/sidebar.jsx | 15 ++-------- 3 files changed, 109 insertions(+), 13 deletions(-) diff --git a/api/post.go b/api/post.go index e1adc1d98..5f9dbc775 100644 --- a/api/post.go +++ b/api/post.go @@ -236,9 +236,68 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo if triggerWebhooks { handleWebhookEventsAndForget(c, post, team, channel, user) } + + if channel.Type == model.CHANNEL_DIRECT { + go makeDirectChannelVisible(c.Session.TeamId, post.ChannelId) + } }() } +func makeDirectChannelVisible(teamId string, channelId string) { + var members []model.ChannelMember + if result := <-Srv.Store.Channel().GetMembers(channelId); result.Err != nil { + l4g.Error("Failed to get channel members channel_id=%v err=%v", channelId, result.Err.Message) + return + } else { + members = result.Data.([]model.ChannelMember) + } + + if len(members) != 2 { + l4g.Error("Failed to get 2 members for a direct channel channel_id=%v", channelId) + return + } + + // make sure the channel is visible to both members + for i, member := range members { + otherUserId := members[1-i].UserId + + if result := <-Srv.Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId); result.Err != nil { + // create a new preference since one doesn't exist yet + preference := &model.Preference{ + UserId: member.UserId, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, + Name: otherUserId, + Value: "true", + } + + if saveResult := <-Srv.Store.Preference().Save(&model.Preferences{*preference}); saveResult.Err != nil { + l4g.Error("Failed to save direct channel preference user_id=%v other_user_id=%v err=%v", member.UserId, otherUserId, saveResult.Err.Message) + } else { + message := model.NewMessage(teamId, channelId, member.UserId, model.ACTION_PREFERENCE_CHANGED) + message.Add("preference", preference.ToJson()) + + PublishAndForget(message) + } + } else { + preference := result.Data.(model.Preference) + + if preference.Value != "true" { + // update the existing preference to make the channel visible + preference.Value = "true" + + if updateResult := <-Srv.Store.Preference().Save(&model.Preferences{preference}); updateResult.Err != nil { + l4g.Error("Failed to update direct channel preference user_id=%v other_user_id=%v err=%v", member.UserId, otherUserId, updateResult.Err.Message) + } else { + message := model.NewMessage(teamId, channelId, member.UserId, model.ACTION_PREFERENCE_CHANGED) + message.Add("preference", preference.ToJson()) + + PublishAndForget(message) + } + } + } + } +} + func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel, user *model.User) { go func() { if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { diff --git a/api/post_test.go b/api/post_test.go index 0cb437e88..8e09ca76d 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -805,3 +805,51 @@ func TestFuzzyPosts(t *testing.T) { } } } + +func TestMakeDirectChannelVisible(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)) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + // user2 will be created with prefs created to show user1 in the sidebar so set that to false to get rid of it + Client.LoginByEmail(team.Name, user2.Email, "pwd") + + preferences := &model.Preferences{ + { + UserId: user2.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, + Name: user1.Id, + Value: "false", + }, + } + Client.Must(Client.SetPreferences(preferences)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel := Client.Must(Client.CreateDirectChannel(map[string]string{"user_id": user2.Id})).Data.(*model.Channel) + + makeDirectChannelVisible(team.Id, channel.Id) + + if result, err := Client.GetPreference(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, user2.Id); err != nil { + t.Fatal("Errored trying to set direct channel to be visible for user1") + } else if pref := result.Data.(*model.Preference); pref.Value != "true" { + t.Fatal("Failed to set direct channel to be visible for user1") + } + + Client.LoginByEmail(team.Name, user2.Email, "pwd") + + if result, err := Client.GetPreference(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, user1.Id); err != nil { + t.Fatal("Errored trying to set direct channel to be visible for user2") + } else if pref := result.Data.(*model.Preference); pref.Value != "true" { + t.Fatal("Failed to set direct channel to be visible for user2") + } +} diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 3d7f449d1..b9835ae11 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -89,25 +89,14 @@ export default class Sidebar extends React.Component { continue; } - const member = members[dm.id]; - const msgCount = dm.total_msg_count - member.msg_count; + const show = preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false')); - // always show a channel if either it is the current one or if it is unread, but it is not currently being left - const forceShow = (currentChannelId === dm.id || msgCount > 0) && !this.isLeaving.get(dm.id); - const preferenceShow = preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false')); - - if (preferenceShow || forceShow) { + if (show) { dm.display_name = Utils.displayUsername(teammate.id); dm.teammate_id = teammate.id; dm.status = UserStore.getStatus(teammate.id); visibleDirectChannels.push(dm); - - if (forceShow && !preferenceShow) { - // make sure that unread direct channels are visible - const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true'); - AsyncClient.savePreferences([preference]); - } } } -- cgit v1.2.3-1-g7c22 From c55e7895d91c9d4cd31c7cefe81319d64d7fed16 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Tue, 8 Dec 2015 09:45:21 -0500 Subject: Changed Sidebar direct channel list to be driven by preferences --- web/react/components/sidebar.jsx | 41 ++++++++++++++++++++++++---------------- web/react/utils/utils.jsx | 7 ++++++- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index b9835ae11..8393440cb 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -71,38 +71,47 @@ export default class Sidebar extends React.Component { getStateFromStores() { const members = ChannelStore.getAllMembers(); const currentChannelId = ChannelStore.getCurrentId(); + const currentUserId = UserStore.getCurrentId(); const channels = Object.assign([], ChannelStore.getAll()); channels.sort((a, b) => a.display_name.localeCompare(b.display_name)); const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL); const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL); - const directChannels = channels.filter((channel) => channel.type === Constants.DM_CHANNEL); const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); - var visibleDirectChannels = []; - for (var i = 0; i < directChannels.length; i++) { - const dm = directChannels[i]; - const teammate = Utils.getDirectTeammate(dm.id); - if (!teammate) { + const directChannels = []; + for (const preference of preferences) { + if (preference.value !== 'true') { continue; } - const show = preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false')); + const teammateId = preference.name; - if (show) { - dm.display_name = Utils.displayUsername(teammate.id); - dm.teammate_id = teammate.id; - dm.status = UserStore.getStatus(teammate.id); + let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId)); - visibleDirectChannels.push(dm); + // a direct channel doesn't exist yet so create a fake one + if (!directChannel) { + directChannel = { + name: Utils.getDirectChannelName(currentUserId, teammateId), + last_post_at: 0, + total_msg_count: 0, + type: Constants.DM_CHANNEL, + fake: true + }; } + + directChannel.display_name = Utils.displayUsername(teammateId); + directChannel.teammate_id = teammateId; + directChannel.status = UserStore.getStatus(teammateId); + + directChannels.push(directChannel); } - const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - visibleDirectChannels.length; + directChannels.sort(this.sortChannelsByDisplayName); - visibleDirectChannels.sort(this.sortChannelsByDisplayName); + const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - directChannels.length; const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); @@ -111,7 +120,7 @@ export default class Sidebar extends React.Component { members, publicChannels, privateChannels, - visibleDirectChannels, + directChannels, hiddenDirectChannelCount, unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER @@ -473,7 +482,7 @@ export default class Sidebar extends React.Component { const privateChannelItems = this.state.privateChannels.map(this.createChannelElement); - const directMessageItems = this.state.visibleDirectChannels.map((channel, index, arr) => { + const directMessageItems = this.state.directChannels.map((channel, index, arr) => { return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); }); diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index f80da8415..304713528 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -1137,6 +1137,11 @@ export function getUserIdFromChannelName(channel) { return otherUserId; } +// Returns true if the given channel is a direct channel between the current user and the given one +export function isDirectChannelForUser(otherUserId, channel) { + return channel.type === Constants.DM_CHANNEL && getUserIdFromChannelName(channel) === otherUserId; +} + export function importSlack(file, success, error) { var formData = new FormData(); formData.append('file', file, file.name); @@ -1252,4 +1257,4 @@ export function isFeatureEnabled(feature) { export function isSystemMessage(post) { return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0); -} \ No newline at end of file +} -- cgit v1.2.3-1-g7c22