From 7585e16d844d197f12079ff46bcbd0f78d7a698a Mon Sep 17 00:00:00 2001 From: Chetanya Kandhari Date: Thu, 18 Feb 2021 16:38:01 +0530 Subject: [PATCH] Custom status feature (#16835) * Create the system console setting and send to webapp * MI-1145: Add custom status APIs * MI-1145 Add slash commands to set and clear status * Add validation for custom status API * Trim custom status message * Code refactoring - Run gofmt - Rename constants * Remove sendUserUpdated webhook event * Fix recent custom status length * Update error conditions * Disable /status slash command when config setting is off * MI-1155: Create the feature flag for custom status APIs and slash commands * Move recent custom statuses to user preferences (#7) * Move recent custom statuses to user preferences * Code refactoring and feedback changes * Update slash command text and emoji regex * Make the custom status feature flag off by default * Update SetCustomStatus, handle recents not set better * Update status codes * Update slash command handling * Add telementry settings * Fix i18n order * Revert "Fix i18n order" This reverts commit 499f7eaca8180336f5bcca360cc0133365899e08. * Update i18n strings --- api4/status.go | 89 ++++++++++++++++++ app/app_iface.go | 3 + app/command.go | 9 ++ app/opentracing/opentracing_layer.go | 66 +++++++++++++ app/slashcommands/command_custom_status.go | 85 +++++++++++++++++ app/status.go | 84 +++++++++++++++++ config/client.go | 1 + i18n/en.json | 36 +++++++ model/config.go | 5 + model/custom_status.go | 103 +++++++++++++++++++++ model/emoji.go | 6 ++ model/feature_flags.go | 5 + model/preference.go | 6 ++ model/user.go | 10 ++ services/telemetry/telemetry.go | 1 + 15 files changed, 509 insertions(+) create mode 100644 app/slashcommands/command_custom_status.go create mode 100644 model/custom_status.go diff --git a/api4/status.go b/api4/status.go index b6118990f9..36d9606d90 100644 --- a/api4/status.go +++ b/api4/status.go @@ -13,6 +13,9 @@ func (api *API) InitStatus() { api.BaseRoutes.User.Handle("/status", api.ApiSessionRequired(getUserStatus)).Methods("GET") api.BaseRoutes.Users.Handle("/status/ids", api.ApiSessionRequired(getUserStatusesByIds)).Methods("POST") api.BaseRoutes.User.Handle("/status", api.ApiSessionRequired(updateUserStatus)).Methods("PUT") + api.BaseRoutes.User.Handle("/status/custom", api.ApiSessionRequired(updateUserCustomStatus)).Methods("PUT") + api.BaseRoutes.User.Handle("/status/custom", api.ApiSessionRequired(removeUserCustomStatus)).Methods("DELETE") + api.BaseRoutes.User.Handle("/status/custom/recent", api.ApiSessionRequired(removeUserRecentCustomStatus)).Methods("DELETE") } func getUserStatus(c *Context, w http.ResponseWriter, r *http.Request) { @@ -107,3 +110,89 @@ func updateUserStatus(c *Context, w http.ResponseWriter, r *http.Request) { getUserStatus(c, w, r) } + +func updateUserCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + if !c.App.Config().FeatureFlags.CustomUserStatuses || !*c.App.Config().TeamSettings.EnableCustomUserStatuses { + c.Err = model.NewAppError("updateUserCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented) + return + } + + customStatus := model.CustomStatusFromJson(r.Body) + if customStatus == nil || (customStatus.Text == "" && customStatus.Emoji == "") { + c.SetInvalidParam("custom_status") + return + } + + if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + customStatus.TrimMessage() + err := c.App.SetCustomStatus(c.Params.UserId, customStatus) + if err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} + +func removeUserCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + if !c.App.Config().FeatureFlags.CustomUserStatuses || !*c.App.Config().TeamSettings.EnableCustomUserStatuses { + c.Err = model.NewAppError("removeUserCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented) + return + } + + if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + if err := c.App.RemoveCustomStatus(c.Params.UserId); err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} + +func removeUserRecentCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + if !c.App.Config().FeatureFlags.CustomUserStatuses || !*c.App.Config().TeamSettings.EnableCustomUserStatuses { + c.Err = model.NewAppError("removeUserRecentCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented) + return + } + + recentCustomStatus := model.CustomStatusFromJson(r.Body) + if recentCustomStatus == nil { + c.SetInvalidParam("recent_custom_status") + return + } + + if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + if err := c.App.RemoveRecentCustomStatus(c.Params.UserId, recentCustomStatus); err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} diff --git a/app/app_iface.go b/app/app_iface.go index 7e3fbe97f7..feea73eea0 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -847,12 +847,14 @@ type AppIface interface { ReloadConfig() error RemoveAllDeactivatedMembersFromChannel(channel *model.Channel) *model.AppError RemoveConfigListener(id string) + RemoveCustomStatus(userID string) *model.AppError RemoveDirectory(path string) *model.AppError RemoveFile(path string) *model.AppError RemoveLdapPrivateCertificate() *model.AppError RemoveLdapPublicCertificate() *model.AppError RemovePlugin(id string) *model.AppError RemovePluginFromData(data model.PluginEventData) + RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError RemoveSamlIdpCertificate() *model.AppError RemoveSamlPrivateCertificate() *model.AppError RemoveSamlPublicCertificate() *model.AppError @@ -929,6 +931,7 @@ type AppIface interface { SetActiveChannel(userID string, channelId string) *model.AppError SetAutoResponderStatus(user *model.User, oldNotifyProps model.StringMap) SetContext(c context.Context) + SetCustomStatus(userID string, cs *model.CustomStatus) *model.AppError SetDefaultProfileImage(user *model.User) *model.AppError SetIpAddress(s string) SetPath(s string) diff --git a/app/command.go b/app/command.go index 76ac68e929..b7c8590aef 100644 --- a/app/command.go +++ b/app/command.go @@ -22,6 +22,10 @@ import ( "github.com/mattermost/mattermost-server/v5/utils" ) +const ( + CmdCustomStatusTrigger = "status" +) + type CommandProvider interface { GetTrigger() string GetCommand(a *App, T goi18n.TranslateFunc) *model.Command @@ -80,6 +84,11 @@ func (a *App) ListAutocompleteCommands(teamID string, T goi18n.TranslateFunc) ([ commands := make([]*model.Command, 0, 32) seen := make(map[string]bool) + // Disable custom status slash command if the feature or the setting is off + if !a.Config().FeatureFlags.CustomUserStatuses || !*a.Config().TeamSettings.EnableCustomUserStatuses { + seen[CmdCustomStatusTrigger] = true + } + for _, cmd := range a.PluginCommandsForTeam(teamID) { if cmd.AutoComplete && !seen[cmd.Trigger] { seen[cmd.Trigger] = true diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 83bccf2bde..537615e8df 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -11880,6 +11880,28 @@ func (a *OpenTracingAppLayer) RemoveConfigListener(id string) { a.app.RemoveConfigListener(id) } +func (a *OpenTracingAppLayer) RemoveCustomStatus(userID string) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveCustomStatus") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.RemoveCustomStatus(userID) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) RemoveDirectory(path string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveDirectory") @@ -12005,6 +12027,28 @@ func (a *OpenTracingAppLayer) RemovePluginFromData(data model.PluginEventData) { a.app.RemovePluginFromData(data) } +func (a *OpenTracingAppLayer) RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveRecentCustomStatus") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.RemoveRecentCustomStatus(userID, status) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) RemoveSamlIdpCertificate() *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveSamlIdpCertificate") @@ -13746,6 +13790,28 @@ func (a *OpenTracingAppLayer) SetBotIconImageFromMultiPartFile(botUserId string, return resultVar0 } +func (a *OpenTracingAppLayer) SetCustomStatus(userID string, cs *model.CustomStatus) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetCustomStatus") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.SetCustomStatus(userID, cs) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) SetDefaultProfileImage(user *model.User) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetDefaultProfileImage") diff --git a/app/slashcommands/command_custom_status.go b/app/slashcommands/command_custom_status.go new file mode 100644 index 0000000000..a546e9805b --- /dev/null +++ b/app/slashcommands/command_custom_status.go @@ -0,0 +1,85 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package slashcommands + +import ( + "strings" + + goi18n "github.com/mattermost/go-i18n/i18n" + + "github.com/mattermost/mattermost-server/v5/app" + "github.com/mattermost/mattermost-server/v5/mlog" + "github.com/mattermost/mattermost-server/v5/model" +) + +type CustomStatusProvider struct { +} + +const ( + CmdCustomStatus = app.CmdCustomStatusTrigger + CmdCustomStatusClear = "clear" + + DefaultCustomStatusEmoji = "speech_balloon" +) + +func init() { + app.RegisterCommandProvider(&CustomStatusProvider{}) +} + +func (*CustomStatusProvider) GetTrigger() string { + return CmdCustomStatus +} + +func (*CustomStatusProvider) GetCommand(a *app.App, T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CmdCustomStatus, + AutoComplete: true, + AutoCompleteDesc: T("api.command_custom_status.desc"), + AutoCompleteHint: T("api.command_custom_status.hint"), + DisplayName: T("api.command_custom_status.name"), + } +} + +func (*CustomStatusProvider) DoCommand(a *app.App, args *model.CommandArgs, message string) *model.CommandResponse { + if !a.Config().FeatureFlags.CustomUserStatuses || !*a.Config().TeamSettings.EnableCustomUserStatuses { + return nil + } + + if message == CmdCustomStatusClear { + if err := a.RemoveCustomStatus(args.UserId); err != nil { + mlog.Error(err.Error()) + return &model.CommandResponse{Text: args.T("api.command_custom_status.clear.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + return &model.CommandResponse{ + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + Text: args.T("api.command_custom_status.clear.success"), + } + } + + customStatus := &model.CustomStatus{ + Emoji: DefaultCustomStatusEmoji, + Text: message, + } + firstEmojiLocations := model.ALL_EMOJI_PATTERN.FindIndex([]byte(message)) + if len(firstEmojiLocations) > 0 && firstEmojiLocations[0] == 0 { + // emoji found at starting index + customStatus.Emoji = message[firstEmojiLocations[0]+1 : firstEmojiLocations[1]-1] + customStatus.Text = strings.TrimSpace(message[firstEmojiLocations[1]:]) + } + + customStatus.TrimMessage() + if err := a.SetCustomStatus(args.UserId, customStatus); err != nil { + mlog.Error(err.Error()) + return &model.CommandResponse{Text: args.T("api.command_custom_status.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + return &model.CommandResponse{ + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + Text: args.T("api.command_custom_status.success", map[string]interface{}{ + "EmojiName": ":" + customStatus.Emoji + ":", + "StatusMessage": customStatus.Text, + }), + } +} diff --git a/app/status.go b/app/status.go index 6436bb5217..fa8e684a32 100644 --- a/app/status.go +++ b/app/status.go @@ -6,6 +6,7 @@ package app import ( "errors" "net/http" + "strings" "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" @@ -363,3 +364,86 @@ func (a *App) GetStatus(userID string) (*model.Status, *model.AppError) { func (a *App) IsUserAway(lastActivityAt int64) bool { return model.GetMillis()-lastActivityAt >= *a.Config().TeamSettings.UserStatusAwayTimeout*1000 } + +func (a *App) SetCustomStatus(userID string, cs *model.CustomStatus) *model.AppError { + user, err := a.GetUser(userID) + if err != nil { + return err + } + + user.SetCustomStatus(cs) + _, updateErr := a.UpdateUser(user, true) + if updateErr != nil { + return err + } + + if err := a.addRecentCustomStatus(userID, cs); err != nil { + a.Log().Error("Can't add recent custom status for", mlog.String("userID", userID), mlog.Err(err)) + } + + return nil +} + +func (a *App) RemoveCustomStatus(userID string) *model.AppError { + user, err := a.GetUser(userID) + if err != nil { + return err + } + + user.ClearCustomStatus() + _, updateErr := a.UpdateUser(user, true) + if updateErr != nil { + return err + } + + return nil +} + +func (a *App) addRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError { + var newRCS *model.RecentCustomStatuses + + pref, err := a.GetPreferenceByCategoryAndNameForUser(userID, model.PREFERENCE_CATEGORY_CUSTOM_STATUS, model.PREFERENCE_NAME_RECENT_CUSTOM_STATUSES) + if err != nil || pref.Value == "" { + newRCS = &model.RecentCustomStatuses{*status} + } else { + existingRCS := model.RecentCustomStatusesFromJson(strings.NewReader(pref.Value)) + newRCS = existingRCS.Add(status) + } + + pref = &model.Preference{ + UserId: userID, + Category: model.PREFERENCE_CATEGORY_CUSTOM_STATUS, + Name: model.PREFERENCE_NAME_RECENT_CUSTOM_STATUSES, + Value: newRCS.ToJson(), + } + if err := a.UpdatePreferences(userID, model.Preferences{*pref}); err != nil { + return err + } + + return nil +} + +func (a *App) RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError { + pref, err := a.GetPreferenceByCategoryAndNameForUser(userID, model.PREFERENCE_CATEGORY_CUSTOM_STATUS, model.PREFERENCE_NAME_RECENT_CUSTOM_STATUSES) + if err != nil { + return err + } + + if pref.Value == "" { + return model.NewAppError("RemoveRecentCustomStatus", "api.custom_status.recent_custom_statuses.delete.app_error", nil, "", http.StatusBadRequest) + } + + existingRCS := model.RecentCustomStatusesFromJson(strings.NewReader(pref.Value)) + if !existingRCS.Contains(status) { + return model.NewAppError("RemoveRecentCustomStatus", "api.custom_status.recent_custom_statuses.delete.app_error", nil, "", http.StatusBadRequest) + } + + newRCS := existingRCS.Remove(status) + pref.Value = newRCS.ToJson() + + if err := a.UpdatePreferences(userID, model.Preferences{*pref}); err != nil { + return err + } + + return nil +} diff --git a/config/client.go b/config/client.go index 7f2ffd87ac..904cbb8c08 100644 --- a/config/client.go +++ b/config/client.go @@ -16,6 +16,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props := GenerateLimitedClientConfig(c, telemetryID, license) props["SiteURL"] = strings.TrimRight(*c.ServiceSettings.SiteURL, "/") + props["EnableCustomUserStatuses"] = strconv.FormatBool(c.FeatureFlags.CustomUserStatuses && *c.TeamSettings.EnableCustomUserStatuses) props["EnableUserDeactivation"] = strconv.FormatBool(*c.TeamSettings.EnableUserDeactivation) props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage props["EnableXToLeaveChannelsFromLHS"] = strconv.FormatBool(*c.TeamSettings.EnableXToLeaveChannelsFromLHS) diff --git a/i18n/en.json b/i18n/en.json index 2144d05f03..7e854e6851 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -715,6 +715,34 @@ "id": "api.command_collapse.success", "translation": "Image links now collapse by default" }, + { + "id": "api.command_custom_status.app_error", + "translation": "Error setting the status." + }, + { + "id": "api.command_custom_status.clear.app_error", + "translation": "Error clearing the status." + }, + { + "id": "api.command_custom_status.clear.success", + "translation": "Your status was cleared." + }, + { + "id": "api.command_custom_status.desc", + "translation": "Set or clear your status" + }, + { + "id": "api.command_custom_status.hint", + "translation": "[:emoji_name:] [status_message] or clear" + }, + { + "id": "api.command_custom_status.name", + "translation": "status" + }, + { + "id": "api.command_custom_status.success", + "translation": "Your status is set to “{{.EmojiName}} {{.StatusMessage}}”. You can change your status from the status popover in the channel sidebar header." + }, { "id": "api.command_dnd.desc", "translation": "Do not disturb disables desktop and mobile push notifications." @@ -1206,6 +1234,14 @@ "id": "api.create_terms_of_service.empty_text.app_error", "translation": "Please enter text for your Custom Terms of Service." }, + { + "id": "api.custom_status.disabled", + "translation": "Custom status feature has been disabled. Please contact your system administrator for details." + }, + { + "id": "api.custom_status.recent_custom_statuses.delete.app_error", + "translation": "Failed to delete the recent status. Please try adding the status first or contact your system administrator for details." + }, { "id": "api.email.send_warn_metric_ack.failure.app_error", "translation": "Failure to send admin acknowledgment email" diff --git a/model/config.go b/model/config.go index 0ed0bb23b6..8d46ca1847 100644 --- a/model/config.go +++ b/model/config.go @@ -1857,6 +1857,7 @@ type TeamSettings struct { EnableOpenServer *bool `access:"authentication"` EnableUserDeactivation *bool `access:"experimental"` RestrictCreationToDomains *string `access:"authentication"` // telemetry: none + EnableCustomUserStatuses *bool `access:"site"` EnableCustomBrand *bool `access:"site"` CustomBrandText *string `access:"site"` CustomDescriptionText *string `access:"site"` @@ -1910,6 +1911,10 @@ func (s *TeamSettings) SetDefaults() { s.RestrictCreationToDomains = NewString("") } + if s.EnableCustomUserStatuses == nil { + s.EnableCustomUserStatuses = NewBool(true) + } + if s.EnableCustomBrand == nil { s.EnableCustomBrand = NewBool(false) } diff --git a/model/custom_status.go b/model/custom_status.go new file mode 100644 index 0000000000..5479144d26 --- /dev/null +++ b/model/custom_status.go @@ -0,0 +1,103 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + UserPropsKeyCustomStatus = "customStatus" + + CustomStatusTextMaxRunes = 100 + MaxRecentCustomStatuses = 5 +) + +type CustomStatus struct { + Emoji string `json:"emoji"` + Text string `json:"text"` +} + +func (cs *CustomStatus) TrimMessage() { + runes := []rune(cs.Text) + if len(runes) > CustomStatusTextMaxRunes { + cs.Text = string(runes[:CustomStatusTextMaxRunes]) + } +} + +func (cs *CustomStatus) ToJson() string { + csCopy := *cs + b, _ := json.Marshal(csCopy) + return string(b) +} + +func CustomStatusFromJson(data io.Reader) *CustomStatus { + var cs *CustomStatus + _ = json.NewDecoder(data).Decode(&cs) + return cs +} + +type RecentCustomStatuses []CustomStatus + +func (rcs *RecentCustomStatuses) Contains(cs *CustomStatus) bool { + var csJSON = cs.ToJson() + + // status is empty + if cs == nil || csJSON == "" || (cs.Emoji == "" && cs.Text == "") { + return false + } + + for _, status := range *rcs { + if status.ToJson() == csJSON { + return true + } + } + + return false +} + +func (rcs *RecentCustomStatuses) Add(cs *CustomStatus) *RecentCustomStatuses { + newRCS := (*rcs)[:0] + + // if same `text` exists in existing recent custom statuses, modify existing status + for _, status := range *rcs { + if status.Text != cs.Text { + newRCS = append(newRCS, status) + } + } + newRCS = append(RecentCustomStatuses{*cs}, newRCS...) + if len(newRCS) > MaxRecentCustomStatuses { + newRCS = newRCS[:MaxRecentCustomStatuses] + } + return &newRCS +} + +func (rcs *RecentCustomStatuses) Remove(cs *CustomStatus) *RecentCustomStatuses { + var csJSON = cs.ToJson() + if csJSON == "" || (cs.Emoji == "" && cs.Text == "") { + return rcs + } + + newRCS := (*rcs)[:0] + for _, status := range *rcs { + if status.ToJson() != csJSON { + newRCS = append(newRCS, status) + } + } + + return &newRCS +} + +func (rcs *RecentCustomStatuses) ToJson() string { + rcsCopy := *rcs + b, _ := json.Marshal(rcsCopy) + return string(b) +} + +func RecentCustomStatusesFromJson(data io.Reader) *RecentCustomStatuses { + var rcs *RecentCustomStatuses + _ = json.NewDecoder(data).Decode(&rcs) + return rcs +} diff --git a/model/emoji.go b/model/emoji.go index 1ac3d12238..bcc35e9ff3 100644 --- a/model/emoji.go +++ b/model/emoji.go @@ -17,6 +17,12 @@ const ( var EMOJI_PATTERN = regexp.MustCompile(`:[a-zA-Z0-9_-]+:`) +// ALL_EMOJI_PATTERN is same as the EMOJI_PATTERN except for allowing a '+' character. +// This is to allow the system emoji :+1: to be matched. +// We kept a separate variable to avoid renaming help texts for custom emoji's. +// TODO: Merge ALL_EMOJI_PATTERN with EMOJI_PATTERN after updating custom emoji help texts +var ALL_EMOJI_PATTERN = regexp.MustCompile(`:[a-zA-Z0-9_+-]+:`) + type Emoji struct { Id string `json:"id"` CreateAt int64 `json:"create_at"` diff --git a/model/feature_flags.go b/model/feature_flags.go index 35ab2d3f95..7be0905de7 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -18,6 +18,10 @@ type FeatureFlags struct { // Toggle on and off support for Collapsed Threads CollapsedThreads bool + + // Toggle on and off support for Custom User Statuses + CustomUserStatuses bool + // Feature flags to control plugin versions PluginIncidentManagement string `plugin_id:"com.mattermost.plugin-incident-management"` } @@ -27,6 +31,7 @@ func (f *FeatureFlags) SetDefaults() { f.TestBoolFeature = false f.CloudDelinquentEmailJobsEnabled = false f.CollapsedThreads = false + f.CustomUserStatuses = false f.PluginIncidentManagement = "1.4.0" } diff --git a/model/preference.go b/model/preference.go index d6a0719e0b..5559cc0ae6 100644 --- a/model/preference.go +++ b/model/preference.go @@ -40,6 +40,12 @@ const ( PREFERENCE_NAME_LAST_CHANNEL = "channel" PREFERENCE_NAME_LAST_TEAM = "team" + PREFERENCE_CATEGORY_CUSTOM_STATUS = "custom_status" + PREFERENCE_NAME_RECENT_CUSTOM_STATUSES = "recent_custom_statuses" + PREFERENCE_NAME_CUSTOM_STATUS_TUTORIAL_STATE = "custom_status_tutorial_state" + + PREFERENCE_CUSTOM_STATUS_MODAL_VIEWED = "custom_status_modal_viewed" + PREFERENCE_CATEGORY_NOTIFICATIONS = "notifications" PREFERENCE_NAME_EMAIL_INTERVAL = "email_interval" diff --git a/model/user.go b/model/user.go index a47265b813..2957bff68c 100644 --- a/model/user.go +++ b/model/user.go @@ -596,6 +596,16 @@ func (u *User) AddNotifyProp(key string, value string) { u.NotifyProps[key] = value } +func (u *User) SetCustomStatus(cs *CustomStatus) { + u.MakeNonNil() + u.Props[UserPropsKeyCustomStatus] = cs.ToJson() +} + +func (u *User) ClearCustomStatus() { + u.MakeNonNil() + u.Props[UserPropsKeyCustomStatus] = "" +} + func (u *User) GetFullName() string { if u.FirstName != "" && u.LastName != "" { return u.FirstName + " " + u.LastName diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index a4b7b449da..77a5d73b33 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -453,6 +453,7 @@ func (ts *TelemetryService) trackConfig() { "restrict_private_channel_deletion": *cfg.TeamSettings.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelDeletion, "enable_open_server": *cfg.TeamSettings.EnableOpenServer, "enable_user_deactivation": *cfg.TeamSettings.EnableUserDeactivation, + "enable_custom_user_statuses": *cfg.TeamSettings.EnableCustomUserStatuses, "enable_custom_brand": *cfg.TeamSettings.EnableCustomBrand, "restrict_direct_message": *cfg.TeamSettings.RestrictDirectMessage, "max_notifications_per_channel": *cfg.TeamSettings.MaxNotificationsPerChannel,