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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
f0e9ec2dd1
Коммит
bfa04c0ab0
45
api/post.go
45
api/post.go
@@ -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)
|
||||||
|
|||||||
10
i18n/en.json
10
i18n/en.json
@@ -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();
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user