PLT-2408 Adds here mention for online users (#3619)

* Added @here mention that notifies online users

* Fixed existing race condition that would sometime cause clients to miss mention count changes

* Added missing localization strings

* Prevent @here from mentioning the user who posted it
Этот коммит содержится в:
Joram Wilander
2016-07-21 10:00:09 -04:00
коммит произвёл GitHub
родитель f0e9ec2dd1
Коммит bfa04c0ab0
10 изменённых файлов: 96 добавлений и 13 удалений

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

@@ -477,6 +477,8 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
mentionedUserIds := make(map[string]bool) mentionedUserIds := make(map[string]bool)
alwaysNotifyUserIds := []string{} alwaysNotifyUserIds := []string{}
hereNotification := false
updateMentionChans := []store.StoreChannel{}
if channel.Type == model.CHANNEL_DIRECT { if channel.Type == model.CHANNEL_DIRECT {
@@ -537,6 +539,11 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
splitMessage := strings.Fields(post.Message) splitMessage := strings.Fields(post.Message)
var userIds []string var userIds []string
for _, word := range splitMessage { for _, word := range splitMessage {
if word == "@here" {
hereNotification = true
continue
}
// Non-case-sensitive check for regular keys // Non-case-sensitive check for regular keys
if ids, match := keywordMap[strings.ToLower(word)]; match { if ids, match := keywordMap[strings.ToLower(word)]; match {
userIds = append(userIds, ids...) userIds = append(userIds, ids...)
@@ -591,7 +598,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
} }
for id := range mentionedUserIds { for id := range mentionedUserIds {
go updateMentionCount(post.ChannelId, id) updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
} }
} }
@@ -624,6 +631,28 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
} }
} }
if hereNotification {
if result := <-Srv.Store.Status().GetOnline(); result.Err != nil {
l4g.Warn(utils.T("api.post.notification.here.warn"), result.Err)
return
} else {
statuses := result.Data.([]*model.Status)
for _, status := range statuses {
if status.UserId == post.UserId {
continue
}
_, profileFound := profileMap[status.UserId]
_, alreadyAdded := mentionedUserIds[status.UserId]
if status.Status == model.STATUS_ONLINE && profileFound && !alreadyAdded {
mentionedUsersList = append(mentionedUsersList, status.UserId)
updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId))
}
}
}
}
sendPushNotifications := false sendPushNotifications := false
if *utils.Cfg.EmailSettings.SendPushNotifications { if *utils.Cfg.EmailSettings.SendPushNotifications {
pushServer := *utils.Cfg.EmailSettings.PushNotificationServer pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
@@ -671,6 +700,14 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
message.Add("mentions", model.ArrayToJson(mentionedUsersList)) message.Add("mentions", model.ArrayToJson(mentionedUsersList))
} }
// Make sure all mention updates are complete to prevent race
// Probably better to batch these DB updates in the future
for _, uchan := range updateMentionChans {
if result := <-uchan; result.Err != nil {
l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err)
}
}
go Publish(message) go Publish(message)
} }
@@ -837,12 +874,6 @@ func sendPushNotification(post *model.Post, user *model.User, channel *model.Cha
} }
} }
func updateMentionCount(channelId, userId string) {
if result := <-Srv.Store.Channel().IncrementMentionCount(channelId, userId); result.Err != nil {
l4g.Error(utils.T("api.post.update_mention_count_and_forget.update_error"), userId, channelId, result.Err)
}
}
func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.Channel, allProfiles map[string]*model.User, members []model.ChannelMember) { func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.Channel, allProfiles map[string]*model.User, members []model.ChannelMember) {
// don't check for out of channel mentions in direct channels // don't check for out of channel mentions in direct channels
if channel.Type == model.CHANNEL_DIRECT { if channel.Type == model.CHANNEL_DIRECT {

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

@@ -55,14 +55,12 @@ func GetAllStatuses() (map[string]interface{}, *model.AppError) {
func SetStatusOnline(userId string, sessionId string) { func SetStatusOnline(userId string, sessionId string) {
broadcast := false broadcast := false
saveStatus := false
var status *model.Status var status *model.Status
var err *model.AppError var err *model.AppError
if status, err = GetStatus(userId); err != nil { if status, err = GetStatus(userId); err != nil {
status = &model.Status{userId, model.STATUS_ONLINE, model.GetMillis()} status = &model.Status{userId, model.STATUS_ONLINE, model.GetMillis()}
broadcast = true broadcast = true
saveStatus = true
} else { } else {
if status.Status != model.STATUS_ONLINE { if status.Status != model.STATUS_ONLINE {
broadcast = true broadcast = true
@@ -76,7 +74,7 @@ func SetStatusOnline(userId string, sessionId string) {
achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, model.GetMillis()) achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, model.GetMillis())
var schan store.StoreChannel var schan store.StoreChannel
if saveStatus { if broadcast {
schan = Srv.Store.Status().SaveOrUpdate(status) schan = Srv.Store.Status().SaveOrUpdate(status)
} else { } else {
schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt) schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt)

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

@@ -927,6 +927,10 @@
"id": "api.post.check_for_out_of_channel_mentions.message.one", "id": "api.post.check_for_out_of_channel_mentions.message.one",
"translation": "{{.Username}} was mentioned, but they did not receive a notification because they do not belong to this channel." "translation": "{{.Username}} was mentioned, but they did not receive a notification because they do not belong to this channel."
}, },
{
"id": "api.post.notification.here.warn",
"translation": "Unable to send notification to online users with @here, err=%v"
},
{ {
"id": "api.post.create_post.bad_filename.error", "id": "api.post.create_post.bad_filename.error",
"translation": "Bad filename discarded, filename=%v" "translation": "Bad filename discarded, filename=%v"
@@ -1077,7 +1081,7 @@
}, },
{ {
"id": "api.post.update_mention_count_and_forget.update_error", "id": "api.post.update_mention_count_and_forget.update_error",
"translation": "Failed to update mention count for user_id=%v on channel_id=%v err=%v" "translation": "Failed to update mention count, post_id=%v channel_id=%v err=%v"
}, },
{ {
"id": "api.post.update_post.find.app_error", "id": "api.post.update_post.find.app_error",
@@ -3903,6 +3907,10 @@
"id": "store.sql_status.get_online_away.app_error", "id": "store.sql_status.get_online_away.app_error",
"translation": "Encountered an error retrieving all the online/away statuses" "translation": "Encountered an error retrieving all the online/away statuses"
}, },
{
"id": "store.sql_status.get_online.app_error",
"translation": "Encountered an error retrieving all the online statuses"
},
{ {
"id": "store.sql_status.get_total_active_users_count.app_error", "id": "store.sql_status.get_total_active_users_count.app_error",
"translation": "We could not count the active users" "translation": "We could not count the active users"

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

@@ -98,7 +98,27 @@ func (s SqlStatusStore) GetOnlineAway() StoreChannel {
var statuses []*model.Status var statuses []*model.Status
if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE Status = :Online OR Status = :Away", map[string]interface{}{"Online": model.STATUS_ONLINE, "Away": model.STATUS_AWAY}); err != nil { if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE Status = :Online OR Status = :Away", map[string]interface{}{"Online": model.STATUS_ONLINE, "Away": model.STATUS_AWAY}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.GetOnline", "store.sql_status.get_online_away.app_error", nil, err.Error()) result.Err = model.NewLocAppError("SqlStatusStore.GetOnlineAway", "store.sql_status.get_online_away.app_error", nil, err.Error())
} else {
result.Data = statuses
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlStatusStore) GetOnline() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var statuses []*model.Status
if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE Status = :Online", map[string]interface{}{"Online": model.STATUS_ONLINE}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.GetOnline", "store.sql_status.get_online.app_error", nil, err.Error())
} else { } else {
result.Data = statuses result.Data = statuses
} }

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

@@ -48,6 +48,17 @@ func TestSqlStatusStore(t *testing.T) {
} }
} }
if result := <-store.Status().GetOnline(); result.Err != nil {
t.Fatal(result.Err)
} else {
statuses := result.Data.([]*model.Status)
for _, status := range statuses {
if status.Status != model.STATUS_ONLINE {
t.Fatal("should not have returned offline statuses")
}
}
}
if err := (<-store.Status().ResetAll()).Err; err != nil { if err := (<-store.Status().ResetAll()).Err; err != nil {
t.Fatal(err) t.Fatal(err)
} }

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

@@ -266,6 +266,7 @@ type StatusStore interface {
SaveOrUpdate(status *model.Status) StoreChannel SaveOrUpdate(status *model.Status) StoreChannel
Get(userId string) StoreChannel Get(userId string) StoreChannel
GetOnlineAway() StoreChannel GetOnlineAway() StoreChannel
GetOnline() StoreChannel
ResetAll() StoreChannel ResetAll() StoreChannel
GetTotalActiveUsersCount() StoreChannel GetTotalActiveUsersCount() StoreChannel
UpdateLastActivityAt(userId string, lastActivityAt int64) StoreChannel UpdateLastActivityAt(userId string, lastActivityAt int64) StoreChannel

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

@@ -43,6 +43,15 @@ class AtMentionSuggestion extends Suggestion {
/> />
); );
icon = <i className='mention__image fa fa-users fa-2x'/>; icon = <i className='mention__image fa fa-users fa-2x'/>;
} else if (user.username === 'here') {
username = 'here';
description = (
<FormattedMessage
id='suggestion.mention.here'
defaultMessage='Notifies everyone in the channel and online'
/>
);
icon = <i className='mention__image fa fa-users fa-2x'/>;
} else { } else {
username = user.username; username = user.username;
@@ -126,6 +135,9 @@ export default class AtMentionProvider {
if ('all'.startsWith(prefix)) { if ('all'.startsWith(prefix)) {
filtered.push({username: 'all'}); filtered.push({username: 'all'});
} }
if ('here'.startsWith(prefix)) {
filtered.push({username: 'here'});
}
} }
filtered.sort((a, b) => { filtered.sort((a, b) => {

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

@@ -1410,6 +1410,7 @@
"sso_signup.team_error": "Please enter a team name", "sso_signup.team_error": "Please enter a team name",
"suggestion.mention.all": "Notifies everyone in the channel, use in {townsquare} to notify the whole team", "suggestion.mention.all": "Notifies everyone in the channel, use in {townsquare} to notify the whole team",
"suggestion.mention.channel": "Notifies everyone in the channel", "suggestion.mention.channel": "Notifies everyone in the channel",
"suggestion.mention.here": "Notifies everyone in the channel and online",
"suggestion.search.private": "Private Groups", "suggestion.search.private": "Private Groups",
"suggestion.search.public": "Public Channels", "suggestion.search.public": "Public Channels",
"team_export_tab.download": "download", "team_export_tab.download": "download",

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

@@ -217,7 +217,7 @@ export const Constants = {
ONLINE: 'online' ONLINE: 'online'
}, },
SPECIAL_MENTIONS: ['all', 'channel'], SPECIAL_MENTIONS: ['all', 'channel', 'here'],
CHARACTER_LIMIT: 4000, CHARACTER_LIMIT: 4000,
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'], IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'], AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'],

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

@@ -207,6 +207,7 @@ function highlightCurrentMentions(text, tokens) {
let output = text; let output = text;
const mentionKeys = UserStore.getCurrentMentionKeys(); const mentionKeys = UserStore.getCurrentMentionKeys();
mentionKeys.push('@here');
// look for any existing tokens which are self mentions and should be highlighted // look for any existing tokens which are self mentions and should be highlighted
var newTokens = new Map(); var newTokens = new Map();