[MM-58834] Review user preferences export and import (#28286)

Этот коммит содержится в:
Ibrahim Serdar Acikgoz
2024-10-11 12:28:37 +02:00
коммит произвёл GitHub
родитель e8ebbcc980
Коммит 9d5993d89d
10 изменённых файлов: 526 добавлений и 140 удалений

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

@@ -29,43 +29,83 @@ import (
// We use this map to identify the exportable preferences. // We use this map to identify the exportable preferences.
// Here we link the preference category and name, to the name of the relevant field in the import struct. // Here we link the preference category and name, to the name of the relevant field in the import struct.
var exportablePreferences = map[imports.ComparablePreference]string{{ var exportablePreferences = map[imports.ComparablePreference]string{
Category: model.PreferenceCategoryTheme, {
Name: "", Category: model.PreferenceCategoryTheme,
}: "Theme", { Name: "",
Category: model.PreferenceCategoryAdvancedSettings, }: "Theme",
Name: "feature_enabled_markdown_preview", {
}: "UseMarkdownPreview", { Category: model.PreferenceCategoryAdvancedSettings,
Category: model.PreferenceCategoryAdvancedSettings, Name: "feature_enabled_markdown_preview",
Name: "formatting", }: "UseMarkdownPreview",
}: "UseFormatting", { {
Category: model.PreferenceCategorySidebarSettings, Category: model.PreferenceCategoryAdvancedSettings,
Name: "show_unread_section", Name: "formatting",
}: "ShowUnreadSection", { }: "UseFormatting",
Category: model.PreferenceCategoryDisplaySettings, {
Name: model.PreferenceNameUseMilitaryTime, Category: model.PreferenceCategoryAdvancedSettings,
}: "UseMilitaryTime", { Name: "send_on_ctrl_enter",
Category: model.PreferenceCategoryDisplaySettings, }: "SendOnCtrlEnter",
Name: model.PreferenceNameCollapseSetting, {
}: "CollapsePreviews", { Category: model.PreferenceCategoryAdvancedSettings,
Category: model.PreferenceCategoryDisplaySettings, Name: "code_block_ctrl_enter",
Name: model.PreferenceNameMessageDisplay, }: "CodeBlockCtrlEnter",
}: "MessageDisplay", { {
Category: model.PreferenceCategoryDisplaySettings, Category: model.PreferenceCategoryAdvancedSettings,
Name: "channel_display_mode", Name: "join_leave",
}: "CollapseConsecutive", { }: "ShowJoinLeave",
Category: model.PreferenceCategoryDisplaySettings, {
Name: "collapse_consecutive_messages", Category: model.PreferenceCategoryAdvancedSettings,
}: "ColorizeUsernames", { Name: "unread_scroll_position",
Category: model.PreferenceCategoryDisplaySettings, }: "ShowUnreadScrollPosition",
Name: "colorize_usernames", {
}: "ChannelDisplayMode", { Category: model.PreferenceCategoryAdvancedSettings,
Category: model.PreferenceCategoryTutorialSteps, Name: "sync_drafts",
Name: "", }: "SyncDrafts",
}: "TutorialStep", { {
Category: model.PreferenceCategoryNotifications, Category: model.PreferenceCategorySidebarSettings,
Name: model.PreferenceNameEmailInterval, Name: model.PreferenceNameShowUnreadSection,
}: "EmailInterval", }: "ShowUnreadSection",
{
Category: model.PreferenceCategorySidebarSettings,
Name: model.PreferenceLimitVisibleDmsGms,
}: "LimitVisibleDmsGms",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameUseMilitaryTime,
}: "UseMilitaryTime",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseSetting,
}: "CollapsePreviews",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameMessageDisplay,
}: "MessageDisplay",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameChannelDisplayMode,
}: "ChannelDisplayMode",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseConsecutive,
}: "CollapseConsecutive",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameColorizeUsernames,
}: "ColorizeUsernames",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameNameFormat,
}: "NameFormat",
{
Category: model.PreferenceCategoryTutorialSteps,
Name: "",
}: "TutorialStep",
{
Category: model.PreferenceCategoryNotifications,
Name: model.PreferenceNameEmailInterval,
}: "EmailInterval",
} }
func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError { func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError {

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

@@ -144,28 +144,35 @@ func ImportLineFromUser(user *model.User, exportedPrefs map[string]*string) *imp
return &imports.LineImportData{ return &imports.LineImportData{
Type: "user", Type: "user",
User: &imports.UserImportData{ User: &imports.UserImportData{
Username: &user.Username, Username: &user.Username,
Email: &user.Email, Email: &user.Email,
AuthService: authService, AuthService: authService,
AuthData: user.AuthData, AuthData: user.AuthData,
Nickname: &user.Nickname, Nickname: &user.Nickname,
FirstName: &user.FirstName, FirstName: &user.FirstName,
LastName: &user.LastName, LastName: &user.LastName,
Position: &user.Position, Position: &user.Position,
Roles: &user.Roles, Roles: &user.Roles,
Locale: &user.Locale, Locale: &user.Locale,
UseMarkdownPreview: exportedPrefs["UseMarkdownPreview"], UseMarkdownPreview: exportedPrefs["UseMarkdownPreview"],
UseFormatting: exportedPrefs["UseFormatting"], UseFormatting: exportedPrefs["UseFormatting"],
ShowUnreadSection: exportedPrefs["ShowUnreadSection"], ShowUnreadSection: exportedPrefs["ShowUnreadSection"],
Theme: exportedPrefs["Theme"], Theme: exportedPrefs["Theme"],
UseMilitaryTime: exportedPrefs["UseMilitaryTime"], UseMilitaryTime: exportedPrefs["UseMilitaryTime"],
CollapsePreviews: exportedPrefs["CollapsePreviews"], CollapsePreviews: exportedPrefs["CollapsePreviews"],
MessageDisplay: exportedPrefs["MessageDisplay"], MessageDisplay: exportedPrefs["MessageDisplay"],
ColorizeUsernames: exportedPrefs["ColorizeUsernames"], ColorizeUsernames: exportedPrefs["ColorizeUsernames"],
ChannelDisplayMode: exportedPrefs["ChannelDisplayMode"], ChannelDisplayMode: exportedPrefs["ChannelDisplayMode"],
TutorialStep: exportedPrefs["TutorialStep"], TutorialStep: exportedPrefs["TutorialStep"],
EmailInterval: exportedPrefs["EmailInterval"], EmailInterval: exportedPrefs["EmailInterval"],
DeleteAt: &user.DeleteAt, NameFormat: exportedPrefs["NameFormat"],
SendOnCtrlEnter: exportedPrefs["SendOnCtrlEnter"],
ShowJoinLeave: exportedPrefs["ShowJoinLeave"],
SyncDrafts: exportedPrefs["SyncDrafts"],
ShowUnreadScrollPosition: exportedPrefs["ShowUnreadScrollPosition"],
LimitVisibleDmsGms: exportedPrefs["LimitVisibleDmsGms"],
CodeBlockCtrlEnter: exportedPrefs["CodeBlockCtrlEnter"],
DeleteAt: &user.DeleteAt,
}, },
} }
} }
@@ -238,17 +245,20 @@ func ImportUserChannelDataFromChannelMemberAndPreferences(member *model.ChannelM
} }
func ImportLineForPost(post *model.PostForExport) *imports.LineImportData { func ImportLineForPost(post *model.PostForExport) *imports.LineImportData {
f := []string(post.FlaggedBy)
return &imports.LineImportData{ return &imports.LineImportData{
Type: "post", Type: "post",
Post: &imports.PostImportData{ Post: &imports.PostImportData{
Team: &post.TeamName, Team: &post.TeamName,
Channel: &post.ChannelName, Channel: &post.ChannelName,
User: &post.Username, User: &post.Username,
Type: &post.Type, Type: &post.Type,
Message: &post.Message, Message: &post.Message,
Props: &post.Props, Props: &post.Props,
CreateAt: &post.CreateAt, CreateAt: &post.CreateAt,
EditAt: &post.EditAt, EditAt: &post.EditAt,
IsPinned: &post.IsPinned,
FlaggedBy: &f,
}, },
} }
} }
@@ -258,6 +268,7 @@ func ImportLineForDirectPost(post *model.DirectPostForExport) *imports.LineImpor
if len(channelMembers) == 1 { if len(channelMembers) == 1 {
channelMembers = []string{channelMembers[0], channelMembers[0]} channelMembers = []string{channelMembers[0], channelMembers[0]}
} }
f := []string(post.FlaggedBy)
return &imports.LineImportData{ return &imports.LineImportData{
Type: "direct_post", Type: "direct_post",
DirectPost: &imports.DirectPostImportData{ DirectPost: &imports.DirectPostImportData{
@@ -268,17 +279,22 @@ func ImportLineForDirectPost(post *model.DirectPostForExport) *imports.LineImpor
Props: &post.Props, Props: &post.Props,
CreateAt: &post.CreateAt, CreateAt: &post.CreateAt,
EditAt: &post.EditAt, EditAt: &post.EditAt,
IsPinned: &post.IsPinned,
FlaggedBy: &f,
}, },
} }
} }
func ImportReplyFromPost(post *model.ReplyForExport) *imports.ReplyImportData { func ImportReplyFromPost(post *model.ReplyForExport) *imports.ReplyImportData {
f := []string(post.FlaggedBy)
return &imports.ReplyImportData{ return &imports.ReplyImportData{
User: &post.Username, User: &post.Username,
Type: &post.Type, Type: &post.Type,
Message: &post.Message, Message: &post.Message,
CreateAt: &post.CreateAt, CreateAt: &post.CreateAt,
EditAt: &post.EditAt, EditAt: &post.EditAt,
IsPinned: &post.IsPinned,
FlaggedBy: &f,
} }
} }

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

@@ -722,7 +722,7 @@ func (a *App) importUser(rctx request.CTX, data *imports.UserImportData, dryRun
preferences = append(preferences, model.Preference{ preferences = append(preferences, model.Preference{
UserId: savedUser.Id, UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings, Category: model.PreferenceCategoryDisplaySettings,
Name: "channel_display_mode", Name: model.PreferenceNameChannelDisplayMode,
Value: *data.ChannelDisplayMode, Value: *data.ChannelDisplayMode,
}) })
} }
@@ -763,6 +763,69 @@ func (a *App) importUser(rctx request.CTX, data *imports.UserImportData, dryRun
}) })
} }
if data.SendOnCtrlEnter != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "send_on_ctrl_enter",
Value: *data.SendOnCtrlEnter,
})
}
if data.CodeBlockCtrlEnter != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "code_block_ctrl_enter",
Value: *data.CodeBlockCtrlEnter,
})
}
if data.ShowJoinLeave != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "join_leave",
Value: *data.ShowJoinLeave,
})
}
if data.ShowUnreadScrollPosition != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "unread_scroll_position",
Value: *data.ShowUnreadScrollPosition,
})
}
if data.SyncDrafts != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "sync_drafts",
Value: *data.SyncDrafts,
})
}
if data.LimitVisibleDmsGms != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategorySidebarSettings,
Name: model.PreferenceLimitVisibleDmsGms,
Value: *data.LimitVisibleDmsGms,
})
}
if data.NameFormat != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameNameFormat,
Value: *data.NameFormat,
})
}
if data.EmailInterval != nil || savedUser.NotifyProps[model.EmailNotifyProp] == "false" { if data.EmailInterval != nil || savedUser.NotifyProps[model.EmailNotifyProp] == "false" {
var intervalSeconds string var intervalSeconds string
if value := savedUser.NotifyProps[model.EmailNotifyProp]; value == "false" { if value := savedUser.NotifyProps[model.EmailNotifyProp]; value == "false" {
@@ -1184,6 +1247,9 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po
return err return err
} }
usernames = append(usernames, *replyData.User) usernames = append(usernames, *replyData.User)
if replyData.FlaggedBy != nil {
usernames = append(usernames, *replyData.FlaggedBy...)
}
} }
users, err := a.getUsersByUsernames(usernames) users, err := a.getUsersByUsernames(usernames)
@@ -1233,6 +1299,9 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po
if replyData.EditAt != nil { if replyData.EditAt != nil {
reply.EditAt = *replyData.EditAt reply.EditAt = *replyData.EditAt
} }
if replyData.IsPinned != nil {
reply.IsPinned = *replyData.IsPinned
}
fileIDs := a.uploadAttachments(rctx, replyData.Attachments, reply, teamID, extractContent) fileIDs := a.uploadAttachments(rctx, replyData.Attachments, reply, teamID, extractContent)
for _, fileID := range reply.FileIds { for _, fileID := range reply.FileIds {
@@ -1274,6 +1343,27 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po
for _, postWithData := range postsWithData { for _, postWithData := range postsWithData {
a.updateFileInfoWithPostId(rctx, postWithData.post) a.updateFileInfoWithPostId(rctx, postWithData.post)
if postWithData.replyData.FlaggedBy != nil {
var preferences model.Preferences
for _, username := range *postWithData.replyData.FlaggedBy {
user := users[strings.ToLower(username)]
preferences = append(preferences, model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: postWithData.post.Id,
Value: "true",
})
}
if len(preferences) > 0 {
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
} }
return nil return nil

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

@@ -1166,19 +1166,26 @@ func TestImportImportUser(t *testing.T) {
// Add a user with some preferences. // Add a user with some preferences.
username = model.NewUsername() username = model.NewUsername()
data = imports.UserImportData{ data = imports.UserImportData{
Username: &username, Username: &username,
Email: ptrStr(model.NewId() + "@example.com"), Email: ptrStr(model.NewId() + "@example.com"),
Theme: ptrStr(`{"awayIndicator":"#DCBD4E","buttonBg":"#23A2FF","buttonColor":"#FFFFFF","centerChannelBg":"#ffffff","centerChannelColor":"#333333","codeTheme":"github","image":"/static/files/a4a388b38b32678e83823ef1b3e17766.png","linkColor":"#2389d7","mentionBg":"#2389d7","mentionColor":"#ffffff","mentionHighlightBg":"#fff2bb","mentionHighlightLink":"#2f81b7","newMessageSeparator":"#FF8800","onlineIndicator":"#7DBE00","sidebarBg":"#fafafa","sidebarHeaderBg":"#3481B9","sidebarHeaderTextColor":"#ffffff","sidebarText":"#333333","sidebarTextActiveBorder":"#378FD2","sidebarTextActiveColor":"#111111","sidebarTextHoverBg":"#e6f2fa","sidebarUnreadText":"#333333","type":"Mattermost"}`), Theme: ptrStr(`{"awayIndicator":"#DCBD4E","buttonBg":"#23A2FF","buttonColor":"#FFFFFF","centerChannelBg":"#ffffff","centerChannelColor":"#333333","codeTheme":"github","image":"/static/files/a4a388b38b32678e83823ef1b3e17766.png","linkColor":"#2389d7","mentionBg":"#2389d7","mentionColor":"#ffffff","mentionHighlightBg":"#fff2bb","mentionHighlightLink":"#2f81b7","newMessageSeparator":"#FF8800","onlineIndicator":"#7DBE00","sidebarBg":"#fafafa","sidebarHeaderBg":"#3481B9","sidebarHeaderTextColor":"#ffffff","sidebarText":"#333333","sidebarTextActiveBorder":"#378FD2","sidebarTextActiveColor":"#111111","sidebarTextHoverBg":"#e6f2fa","sidebarUnreadText":"#333333","type":"Mattermost"}`),
UseMilitaryTime: ptrStr("true"), UseMilitaryTime: ptrStr("true"),
CollapsePreviews: ptrStr("true"), CollapsePreviews: ptrStr("true"),
MessageDisplay: ptrStr("compact"), MessageDisplay: ptrStr("compact"),
ColorizeUsernames: ptrStr("true"), ColorizeUsernames: ptrStr("true"),
ChannelDisplayMode: ptrStr("centered"), ChannelDisplayMode: ptrStr("centered"),
TutorialStep: ptrStr("3"), TutorialStep: ptrStr("3"),
UseMarkdownPreview: ptrStr("true"), UseMarkdownPreview: ptrStr("true"),
UseFormatting: ptrStr("true"), UseFormatting: ptrStr("true"),
ShowUnreadSection: ptrStr("true"), ShowUnreadSection: ptrStr("true"),
EmailInterval: ptrStr("immediately"), EmailInterval: ptrStr("immediately"),
NameFormat: model.NewPointer("full_name"),
SendOnCtrlEnter: model.NewPointer("true"),
CodeBlockCtrlEnter: model.NewPointer("true"),
ShowJoinLeave: model.NewPointer("false"),
SyncDrafts: model.NewPointer("false"),
ShowUnreadScrollPosition: model.NewPointer("start_from_newest"),
LimitVisibleDmsGms: model.NewPointer("20"),
} }
appErr = th.App.importUser(th.Context, &data, false) appErr = th.App.importUser(th.Context, &data, false)
assert.Nil(t, appErr) assert.Nil(t, appErr)
@@ -1197,7 +1204,13 @@ func TestImportImportUser(t *testing.T) {
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "feature_enabled_markdown_preview", *data.UseMarkdownPreview) checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "feature_enabled_markdown_preview", *data.UseMarkdownPreview)
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "formatting", *data.UseFormatting) checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "formatting", *data.UseFormatting)
checkPreference(t, th.App, user.Id, model.PreferenceCategorySidebarSettings, "show_unread_section", *data.ShowUnreadSection) checkPreference(t, th.App, user.Id, model.PreferenceCategorySidebarSettings, "show_unread_section", *data.ShowUnreadSection)
checkPreference(t, th.App, user.Id, model.PreferenceCategoryNotifications, model.PreferenceNameEmailInterval, "30") checkPreference(t, th.App, user.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameNameFormat, "full_name")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "send_on_ctrl_enter", "true")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "code_block_ctrl_enter", "true")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "join_leave", "false")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "sync_drafts", "false")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "unread_scroll_position", "start_from_newest")
checkPreference(t, th.App, user.Id, model.PreferenceCategorySidebarSettings, model.PreferenceLimitVisibleDmsGms, "20")
// Change those preferences. // Change those preferences.
data = imports.UserImportData{ data = imports.UserImportData{
@@ -2556,6 +2569,44 @@ func TestImportimportMultiplePostLines(t *testing.T) {
AssertAllPostsCount(t, th.App, initialPostCountForTeam2, 1, team2.Id) AssertAllPostsCount(t, th.App, initialPostCountForTeam2, 1, team2.Id)
AssertAllPostsCount(t, th.App, initialPostCount, 15, team.Id) AssertAllPostsCount(t, th.App, initialPostCount, 15, team.Id)
t.Run("Importing a post with a reply both pinned", func(t *testing.T) {
// Create a thread.
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
replyCreate := time.Now().Add(-30 * time.Second).UnixMilli()
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
Team: &teamName,
Channel: &channelName,
User: &user.Username,
Message: model.NewPointer("Thread Message"),
CreateAt: model.NewPointer(importCreate),
IsPinned: model.NewPointer(true),
Replies: &[]imports.ReplyImportData{{
User: &user.Username,
Message: model.NewPointer("Reply"),
CreateAt: model.NewPointer(replyCreate),
IsPinned: model.NewPointer(true),
}},
},
},
LineNumber: 1,
}
_, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.Nil(t, err)
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, importCreate)
require.NoError(t, nErr)
require.Equal(t, 1, len(resultPosts))
require.True(t, resultPosts[0].IsPinned)
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, replyCreate)
require.NoError(t, nErr)
require.Equal(t, 1, len(resultPosts))
require.True(t, resultPosts[0].IsPinned)
})
t.Run("Importing a post with a thread", func(t *testing.T) { t.Run("Importing a post with a thread", func(t *testing.T) {
// Create a thread. // Create a thread.
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli() importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
@@ -2715,6 +2766,76 @@ func TestImportimportMultiplePostLines(t *testing.T) {
assert.ElementsMatch(t, []string{user.Id}, followers) assert.ElementsMatch(t, []string{user.Id}, followers)
}) })
t.Run("Importing a post that someone flagged", func(t *testing.T) {
// Create a thread.
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
Team: &teamName,
Channel: &channelName,
User: &user.Username,
Message: model.NewPointer("Flagged Message"),
CreateAt: model.NewPointer(importCreate),
FlaggedBy: &[]string{user.Username},
},
},
LineNumber: 1,
}
errLine, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.Nil(t, err)
require.Equal(t, 0, errLine)
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, importCreate)
require.NoError(t, nErr)
require.Equal(t, 1, len(resultPosts))
pref, err := th.App.ch.srv.Store().Preference().GetCategoryAndName(model.PreferenceCategoryFlaggedPost, resultPosts[0].Id)
require.NoError(t, err)
require.Len(t, pref, 1)
assert.Equal(t, user.Id, pref[0].UserId)
})
t.Run("Importing a post that someone flagged its replies", func(t *testing.T) {
// Create a thread.
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
replyCreate := time.Now().Add(-30 * time.Second).UnixMilli()
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
Team: &teamName,
Channel: &channelName,
User: &user.Username,
Message: model.NewPointer("Flagged Message"),
CreateAt: model.NewPointer(importCreate),
Replies: &[]imports.ReplyImportData{{
User: &user.Username,
Message: model.NewPointer("Reply"),
CreateAt: model.NewPointer(replyCreate),
FlaggedBy: &[]string{user2.Username},
}},
},
},
LineNumber: 1,
}
errLine, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.Nil(t, err)
require.Equal(t, 0, errLine)
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, replyCreate)
require.NoError(t, nErr)
require.Equal(t, 1, len(resultPosts))
pref, err := th.App.ch.srv.Store().Preference().GetCategoryAndName(model.PreferenceCategoryFlaggedPost, resultPosts[0].Id)
require.NoError(t, err)
require.Len(t, pref, 1)
assert.Equal(t, user2.Id, pref[0].UserId)
})
} }
func TestImportImportPost(t *testing.T) { func TestImportImportPost(t *testing.T) {

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

@@ -73,6 +73,14 @@ type UserImportData struct {
ShowUnreadSection *string `json:"show_unread_section,omitempty"` ShowUnreadSection *string `json:"show_unread_section,omitempty"`
DeleteAt *int64 `json:"delete_at,omitempty"` DeleteAt *int64 `json:"delete_at,omitempty"`
SendOnCtrlEnter *string `json:"send_on_ctrl_enter,omitempty"`
CodeBlockCtrlEnter *string `json:"code_block_ctrl_enter,omitempty"`
ShowJoinLeave *string `json:"show_join_leave,omitempty"`
ShowUnreadScrollPosition *string `json:"show_unread_scroll_position,omitempty"`
SyncDrafts *string `json:"sync_drafts,omitempty"`
LimitVisibleDmsGms *string `json:"limit_visible_dms_gms,omitempty"`
NameFormat *string `json:"name_format,omitempty"`
Teams *[]UserTeamImportData `json:"teams,omitempty"` Teams *[]UserTeamImportData `json:"teams,omitempty"`
Theme *string `json:"theme,omitempty"` Theme *string `json:"theme,omitempty"`
@@ -169,6 +177,7 @@ type ReplyImportData struct {
FlaggedBy *[]string `json:"flagged_by,omitempty"` FlaggedBy *[]string `json:"flagged_by,omitempty"`
Reactions *[]ReactionImportData `json:"reactions,omitempty"` Reactions *[]ReactionImportData `json:"reactions,omitempty"`
Attachments *[]AttachmentImportData `json:"attachments,omitempty"` Attachments *[]AttachmentImportData `json:"attachments,omitempty"`
IsPinned *bool `json:"is_pinned,omitempty"`
} }
type PostImportData struct { type PostImportData struct {
@@ -210,7 +219,7 @@ type DirectPostImportData struct {
CreateAt *int64 `json:"create_at"` CreateAt *int64 `json:"create_at"`
EditAt *int64 `json:"edit_at"` EditAt *int64 `json:"edit_at"`
FlaggedBy *[]string `json:"flagged_by"` FlaggedBy *[]string `json:"flagged_by,omitempty"`
Reactions *[]ReactionImportData `json:"reactions"` Reactions *[]ReactionImportData `json:"reactions"`
Replies *[]ReplyImportData `json:"replies"` Replies *[]ReplyImportData `json:"replies"`
Attachments *[]AttachmentImportData `json:"attachments"` Attachments *[]AttachmentImportData `json:"attachments"`

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

@@ -295,7 +295,7 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m
a.sendUpdatedUserEvent(*nUser) a.sendUpdatedUserEvent(*nUser)
} }
recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceRecommendedNextSteps, Name: "hide", Value: "false"} recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryRecommendedNextSteps, Name: model.PreferenceNameRecommendedNextStepsHide, Value: "false"}
tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"} tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"}
gmASdmPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategorySystemNotice, Name: "GMasDM", Value: "true"} gmASdmPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategorySystemNotice, Name: "GMasDM", Value: "true"}

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

@@ -107,6 +107,15 @@ func postSliceColumns() []string {
return cols return cols
} }
func postSliceColumnsWithName(name string) []string {
colInfos := postSliceColumnsWithTypes()
cols := make([]string, len(colInfos))
for i, colInfo := range colInfos {
cols[i] = name + "." + colInfo.Name
}
return cols
}
func postSliceCoalesceQuery() string { func postSliceCoalesceQuery() string {
colInfos := postSliceColumnsWithTypes() colInfos := postSliceColumnsWithTypes()
cols := make([]string, len(colInfos)) cols := make([]string, len(colInfos))
@@ -2661,13 +2670,22 @@ func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string, inclu
excludeDeletedCond = append(excludeDeletedCond, sq.Eq{"Channels.DeleteAt": 0}) excludeDeletedCond = append(excludeDeletedCond, sq.Eq{"Channels.DeleteAt": 0})
} }
aggFn := "COALESCE(json_agg(u1.username) FILTER (WHERE u1.username IS NOT NULL), '[]')"
if s.DriverName() == model.DatabaseDriverMysql {
aggFn = "IF (COUNT(u1.Username) = 0, JSON_ARRAY(), JSON_ARRAYAGG(u1.Username))"
}
result := []*model.PostForExport{}
builder := s.getQueryBuilder(). builder := s.getQueryBuilder().
Select("p1.*, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName"). Select(fmt.Sprintf("%s, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName, %s as FlaggedBy", strings.Join(postSliceColumnsWithName("p1"), ", "), aggFn)).
FromSelect(sq.Select("*").From("Posts").Where(sq.Eq{"Posts.Id": rootIds}), "p1"). FromSelect(sq.Select("*").From("Posts").Where(sq.Eq{"Posts.Id": rootIds}), "p1").
LeftJoin("Preferences ON p1.Id = Preferences.Name").
LeftJoin("Users u1 ON Preferences.UserId = u1.Id").
InnerJoin("Channels ON p1.ChannelId = Channels.Id"). InnerJoin("Channels ON p1.ChannelId = Channels.Id").
InnerJoin("Teams ON Channels.TeamId = Teams.Id"). InnerJoin("Teams ON Channels.TeamId = Teams.Id").
InnerJoin("Users ON p1.UserId = Users.Id"). InnerJoin("Users ON p1.UserId = Users.Id").
Where(excludeDeletedCond). Where(excludeDeletedCond).
GroupBy(fmt.Sprintf("%s, Users.Username, Teams.Name, Channels.Name", strings.Join(postSliceColumnsWithName("p1"), ", "))).
OrderBy("p1.Id") OrderBy("p1.Id")
query, args, err := builder.ToSql() query, args, err := builder.ToSql()
@@ -2675,56 +2693,72 @@ func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string, inclu
return nil, errors.Wrap(err, "postsForExport_toSql") return nil, errors.Wrap(err, "postsForExport_toSql")
} }
err = s.GetSearchReplicaX().Select(&postsForExport, query, args...) err = s.GetSearchReplicaX().Select(&result, query, args...)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to find Posts") return nil, errors.Wrap(err, "failed to find Posts")
} }
if len(postsForExport) == 0 { if len(result) == 0 {
// All of the posts were in channels or teams that were deleted. // All of the posts were in channels or teams that were deleted.
// Update the afterId and try again. // Update the afterId and try again.
afterId = rootIds[len(rootIds)-1] afterId = rootIds[len(rootIds)-1]
continue continue
} }
return postsForExport, nil return result, nil
} }
} }
func (s *SqlPostStore) GetRepliesForExport(rootId string) ([]*model.ReplyForExport, error) { func (s *SqlPostStore) GetRepliesForExport(rootId string) ([]*model.ReplyForExport, error) {
posts := []*model.ReplyForExport{} aggFn := "COALESCE(json_agg(u1.username) FILTER (WHERE u1.username IS NOT NULL), '[]')"
err := s.GetSearchReplicaX().Select(&posts, ` if s.DriverName() == model.DatabaseDriverMysql {
SELECT aggFn = "IF (COUNT(u1.Username) = 0, JSON_ARRAY(), JSON_ARRAYAGG(u1.Username))"
Posts.*, }
Users.Username as Username result := []*model.ReplyForExport{}
FROM
Posts qb := s.getQueryBuilder().Select(fmt.Sprintf("Posts.*, u2.Username as Username, %s as FlaggedBy", aggFn)).
INNER JOIN From("Posts").
Users ON Posts.UserId = Users.Id LeftJoin("Preferences ON Posts.Id = Preferences.Name").
WHERE LeftJoin("Users u1 ON Preferences.UserId = u1.Id").
Posts.RootId = ? InnerJoin("Users u2 ON Posts.UserId = u2.Id").
AND Posts.DeleteAt = 0 Where(sq.And{sq.Eq{"Posts.RootId": rootId}, sq.Eq{"Posts.DeleteAt": 0}}).
ORDER BY GroupBy("Posts.Id, u2.Username").
Posts.Id`, rootId) OrderBy("Posts.Id")
query, args, err := qb.ToSql()
if err != nil {
return nil, errors.Wrap(err, "postsForExport_toSql")
}
err = s.GetSearchReplicaX().Select(&result, query, args...)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to find Posts") return nil, errors.Wrap(err, "failed to find Posts")
} }
return posts, nil return result, nil
} }
func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId string, includeArchivedChannels bool) ([]*model.DirectPostForExport, error) { func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId string, includeArchivedChannels bool) ([]*model.DirectPostForExport, error) {
aggFn := "COALESCE(json_agg(u1.username) FILTER (WHERE u1.username IS NOT NULL), '[]')"
if s.DriverName() == model.DatabaseDriverMysql {
aggFn = "IF (COUNT(u1.Username) = 0, JSON_ARRAY(), JSON_ARRAYAGG(u1.Username))"
}
result := []*model.DirectPostForExport{}
query := s.getQueryBuilder(). query := s.getQueryBuilder().
Select("p.*", "Users.Username as User"). Select(fmt.Sprintf("p.*, u2.Username as User, %s as FlaggedBy", aggFn)).
From("Posts p"). From("Posts p").
LeftJoin("Preferences ON p.Id = Preferences.Name").
LeftJoin("Users u1 ON Preferences.UserId = u1.Id").
Join("Channels ON p.ChannelId = Channels.Id"). Join("Channels ON p.ChannelId = Channels.Id").
Join("Users ON p.UserId = Users.Id"). Join("Users u2 ON p.UserId = u2.Id").
Where(sq.And{ Where(sq.And{
sq.Gt{"p.Id": afterId}, sq.Gt{"p.Id": afterId},
sq.Eq{"p.RootId": ""}, sq.Eq{"p.RootId": ""},
sq.Eq{"p.DeleteAt": 0}, sq.Eq{"p.DeleteAt": 0},
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}}, sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
}). }).
GroupBy("p.Id, u2.Username").
OrderBy("p.Id"). OrderBy("p.Id").
Limit(uint64(limit)) Limit(uint64(limit))
@@ -2739,13 +2773,12 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str
return nil, errors.Wrap(err, "post_tosql") return nil, errors.Wrap(err, "post_tosql")
} }
posts := []*model.DirectPostForExport{} if err2 := s.GetReplicaX().Select(&result, queryString, args...); err2 != nil {
if err2 := s.GetReplicaX().Select(&posts, queryString, args...); err2 != nil {
return nil, errors.Wrap(err2, "failed to find Posts") return nil, errors.Wrap(err2, "failed to find Posts")
} }
var channelIds []string var channelIds []string
for _, post := range posts { for _, p := range result {
channelIds = append(channelIds, post.ChannelId) channelIds = append(channelIds, p.ChannelId)
} }
query = s.getQueryBuilder(). query = s.getQueryBuilder().
Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest"). Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest").
@@ -2761,13 +2794,13 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str
} }
channelMembers := []*model.ChannelMemberForExport{} channelMembers := []*model.ChannelMemberForExport{}
if err := s.GetReplicaX().Select(&channelMembers, queryString, args...); err != nil { if err = s.GetReplicaX().Select(&channelMembers, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers") return nil, errors.Wrap(err, "failed to find ChannelMembers")
} }
// Build a map of channels and their posts // Build a map of channels and their posts
postsChannelMap := make(map[string][]*model.DirectPostForExport) postsChannelMap := make(map[string][]*model.DirectPostForExport)
for _, post := range posts { for _, post := range result {
post.ChannelMembers = &[]string{} post.ChannelMembers = &[]string{}
postsChannelMap[post.ChannelId] = append(postsChannelMap[post.ChannelId], post) postsChannelMap[post.ChannelId] = append(postsChannelMap[post.ChannelId], post)
} }
@@ -2784,7 +2817,8 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str
*post.ChannelMembers = channelMembersMap[channelId] *post.ChannelMembers = channelMembersMap[channelId]
} }
} }
return posts, nil
return result, nil
} }
//nolint:unparam //nolint:unparam

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

@@ -4349,6 +4349,28 @@ func testPostStoreGetParentsForExportAfter(t *testing.T, rctx request.CTX, ss st
} }
assert.True(t, found) assert.True(t, found)
}) })
t.Run("with flagged post", func(t *testing.T) {
err := ss.Preference().Save(model.Preferences([]model.Preference{
{
UserId: u1.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: p1.Id,
Value: "true",
},
}))
require.NoError(t, err)
posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26), false)
assert.NoError(t, err)
for _, p := range posts {
if p.Id == p1.Id {
require.NotNil(t, p.FlaggedBy)
assert.Equal(t, model.StringArray([]string{u1.Username}), p.FlaggedBy)
}
}
})
} }
func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.Store) { func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.Store) {
@@ -4394,7 +4416,7 @@ func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.S
r1, err := ss.Post().GetRepliesForExport(p1.Id) r1, err := ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, r1, 1) require.Len(t, r1, 1)
reply1 := r1[0] reply1 := r1[0]
assert.Equal(t, reply1.Id, p2.Id) assert.Equal(t, reply1.Id, p2.Id)
@@ -4409,7 +4431,7 @@ func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.S
r1, err = ss.Post().GetRepliesForExport(p1.Id) r1, err = ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, r1, 1) require.Len(t, r1, 1)
reply1 = r1[0] reply1 = r1[0]
assert.Equal(t, reply1.Id, p2.Id) assert.Equal(t, reply1.Id, p2.Id)

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

@@ -235,17 +235,20 @@ type PostForExport struct {
ChannelName string ChannelName string
Username string Username string
ReplyCount int ReplyCount int
FlaggedBy StringArray
} }
type DirectPostForExport struct { type DirectPostForExport struct {
Post Post
User string User string
ChannelMembers *[]string ChannelMembers *[]string
FlaggedBy StringArray
} }
type ReplyForExport struct { type ReplyForExport struct {
Post Post
Username string Username string
FlaggedBy StringArray
} }
type PostForIndexing struct { type PostForIndexing struct {

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

@@ -13,15 +13,70 @@ import (
) )
const ( const (
// The primary key for the preference table is the combination of User.Id, Category, and Name.
// PreferenceCategoryDirectChannelShow and PreferenceCategoryGroupChannelShow
// are used to store the user's preferences for which channels to show in the sidebar.
// The Name field is the channel ID.
PreferenceCategoryDirectChannelShow = "direct_channel_show" PreferenceCategoryDirectChannelShow = "direct_channel_show"
PreferenceCategoryGroupChannelShow = "group_channel_show" PreferenceCategoryGroupChannelShow = "group_channel_show"
PreferenceCategoryTutorialSteps = "tutorial_step" // PreferenceCategoryTutorialStep is used to store the user's progress in the tutorial.
PreferenceCategoryAdvancedSettings = "advanced_settings" // The Name field is the user ID again (for whatever reason).
PreferenceCategoryFlaggedPost = "flagged_post" PreferenceCategoryTutorialSteps = "tutorial_step"
PreferenceCategoryFavoriteChannel = "favorite_channel" // PreferenceCategoryAdvancedSettings has settings for the user's advanced settings.
PreferenceCategorySidebarSettings = "sidebar_settings" // The Name field is the setting name. Possible values are:
// - "formatting"
// - "send_on_ctrl_enter"
// - "join_leave"
// - "unread_scroll_position"
// - "sync_drafts"
// - "feature_enabled_markdown_preview" <- deprecated in favor of "formatting"
PreferenceCategoryAdvancedSettings = "advanced_settings"
// PreferenceCategoryFlaggedPost is used to store the user's saved posts.
// The Name field is the post ID.
PreferenceCategoryFlaggedPost = "flagged_post"
// PreferenceCategoryFavoriteChannel is used to store the user's favorite channels to be
// shown in the sidebar. The Name field is the channel ID.
PreferenceCategoryFavoriteChannel = "favorite_channel"
// PreferenceCategorySidebarSettings is used to store the user's sidebar settings.
// The Name field is the setting name. (ie. PreferenceNameShowUnreadSection or PreferenceLimitVisibleDmsGms)
PreferenceCategorySidebarSettings = "sidebar_settings"
// PreferenceCategoryDisplaySettings is used to store the user's various display settings.
// The possible Name fields are:
// - PreferenceNameUseMilitaryTime
// - PreferenceNameCollapseSetting
// - PreferenceNameMessageDisplay
// - PreferenceNameCollapseConsecutive
// - PreferenceNameColorizeUsernames
// - PreferenceNameChannelDisplayMode
// - PreferenceNameNameFormat
PreferenceCategoryDisplaySettings = "display_settings"
// PreferenceCategorySystemNotice is used store system admin notices.
// Possible Name values are not defined here. It can be anything with the notice name.
PreferenceCategorySystemNotice = "system_notice"
// Deprecated: PreferenceCategoryLast is not used anymore.
PreferenceCategoryLast = "last"
// PreferenceCategoryCustomStatus is used to store the user's custom status preferences.
// Possible Name values are:
// - PreferenceNameRecentCustomStatuses
// - PreferenceNameCustomStatusTutorialState
// - PreferenceCustomStatusModalViewed
PreferenceCategoryCustomStatus = "custom_status"
// PreferenceCategoryNotifications is used to store the user's notification settings.
// Possible Name values are:
// - PreferenceNameEmailInterval
PreferenceCategoryNotifications = "notifications"
// Deprecated: PreferenceRecommendedNextSteps is not used anymore.
// Use PreferenceCategoryRecommendedNextSteps instead.
// PreferenceRecommendedNextSteps is actually a Category. The only possible
// Name vaule is PreferenceRecommendedNextStepsHide for now.
PreferenceRecommendedNextSteps = PreferenceCategoryRecommendedNextSteps
PreferenceCategoryRecommendedNextSteps = "recommended_next_steps"
// PreferenceCategoryTheme has the name for the team id where theme is set.
PreferenceCategoryTheme = "theme"
PreferenceCategoryDisplaySettings = "display_settings"
PreferenceNameCollapsedThreadsEnabled = "collapsed_reply_threads" PreferenceNameCollapsedThreadsEnabled = "collapsed_reply_threads"
PreferenceNameChannelDisplayMode = "channel_display_mode" PreferenceNameChannelDisplayMode = "channel_display_mode"
PreferenceNameCollapseSetting = "collapse_previews" PreferenceNameCollapseSetting = "collapse_previews"
@@ -30,28 +85,26 @@ const (
PreferenceNameColorizeUsernames = "colorize_usernames" PreferenceNameColorizeUsernames = "colorize_usernames"
PreferenceNameNameFormat = "name_format" PreferenceNameNameFormat = "name_format"
PreferenceNameUseMilitaryTime = "use_military_time" PreferenceNameUseMilitaryTime = "use_military_time"
PreferenceRecommendedNextSteps = "recommended_next_steps"
PreferenceCategorySystemNotice = "system_notice" PreferenceNameShowUnreadSection = "show_unread_section"
PreferenceLimitVisibleDmsGms = "limit_visible_dms_gms"
PreferenceCategoryTheme = "theme" PreferenceMaxLimitVisibleDmsGmsValue = 40
// the name for theme props is the team id MaxPreferenceValueLength = 20000
PreferenceCategoryAuthorizedOAuthApp = "oauth_app" PreferenceCategoryAuthorizedOAuthApp = "oauth_app"
// the name for oauth_app is the client_id and value is the current scope // the name for oauth_app is the client_id and value is the current scope
PreferenceCategoryLast = "last" // Deprecated: PreferenceCategoryLastChannel is not used anymore.
PreferenceNameLastChannel = "channel" PreferenceNameLastChannel = "channel"
PreferenceNameLastTeam = "team" // Deprecated: PreferenceCategoryLastTeam is not used anymore.
PreferenceNameLastTeam = "team"
PreferenceCategoryCustomStatus = "custom_status"
PreferenceNameRecentCustomStatuses = "recent_custom_statuses" PreferenceNameRecentCustomStatuses = "recent_custom_statuses"
PreferenceNameCustomStatusTutorialState = "custom_status_tutorial_state" PreferenceNameCustomStatusTutorialState = "custom_status_tutorial_state"
PreferenceCustomStatusModalViewed = "custom_status_modal_viewed"
PreferenceCustomStatusModalViewed = "custom_status_modal_viewed" PreferenceNameEmailInterval = "email_interval"
PreferenceCategoryNotifications = "notifications"
PreferenceNameEmailInterval = "email_interval"
PreferenceEmailIntervalNoBatchingSeconds = "30" // the "immediate" setting is actually 30s PreferenceEmailIntervalNoBatchingSeconds = "30" // the "immediate" setting is actually 30s
PreferenceEmailIntervalBatchingSeconds = "900" // fifteen minutes is 900 seconds PreferenceEmailIntervalBatchingSeconds = "900" // fifteen minutes is 900 seconds
@@ -62,9 +115,7 @@ const (
PreferenceEmailIntervalHourAsSeconds = "3600" PreferenceEmailIntervalHourAsSeconds = "3600"
PreferenceCloudUserEphemeralInfo = "cloud_user_ephemeral_info" PreferenceCloudUserEphemeralInfo = "cloud_user_ephemeral_info"
PreferenceLimitVisibleDmsGms = "limit_visible_dms_gms" PreferenceNameRecommendedNextStepsHide = "hide"
PreferenceMaxLimitVisibleDmsGmsValue = 40
MaxPreferenceValueLength = 20000
) )
type Preference struct { type Preference struct {