Revert "MM-56201, MM-56280: Suppress typing and emoji events (#25794)" (#26281)

This reverts commit f5ee5463e4.
This commit is contained in:
Agniva De Sarker 2024-02-22 19:56:56 +05:30 committed by GitHub
parent 726d7494b6
commit 6808a1c733
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 186 additions and 469 deletions

View File

@ -25,7 +25,6 @@ import (
"github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/public/utils"
) )
const ( const (
@ -49,10 +48,6 @@ const (
const websocketMessagePluginPrefix = "custom_" const websocketMessagePluginPrefix = "custom_"
// UnsetPresenceIndicator is the value that gets set initially for active channel/
// thread/team. This is done to differentiate it from an explicitly set empty value.
const UnsetPresenceIndicator = "<>"
type pluginWSPostedHook struct { type pluginWSPostedHook struct {
connectionID string connectionID string
userID string userID string
@ -110,19 +105,17 @@ type WebConn struct {
// a reused connection. // a reused connection.
// It's theoretically possible for this number to wrap around. But we // It's theoretically possible for this number to wrap around. But we
// leave that as an edge-case. // leave that as an edge-case.
reuseCount int reuseCount int
sessionToken atomic.Value sessionToken atomic.Value
session atomic.Pointer[model.Session] session atomic.Pointer[model.Session]
connectionID atomic.Value connectionID atomic.Value
activeChannelID atomic.Value activeChannelID atomic.Value
activeTeamID atomic.Value activeTeamID atomic.Value
activeRHSThreadChannelID atomic.Value activeRHSThreadChannelID atomic.Value
activeThreadViewThreadChannelID atomic.Value activeThreadViewThreadChannelID atomic.Value
endWritePump chan struct{}
endWritePump chan struct{} pumpFinished chan struct{}
pumpFinished chan struct{} pluginPosted chan pluginWSPostedHook
pluginPosted chan pluginWSPostedHook
// These counters are to suppress spammy websocket.slow // These counters are to suppress spammy websocket.slow
// and websocket.full logs which happen continuously, if they // and websocket.full logs which happen continuously, if they
@ -243,12 +236,10 @@ func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, runn
wc.SetSessionToken(cfg.Session.Token) wc.SetSessionToken(cfg.Session.Token)
wc.SetSessionExpiresAt(cfg.Session.ExpiresAt) wc.SetSessionExpiresAt(cfg.Session.ExpiresAt)
wc.SetConnectionID(cfg.ConnectionID) wc.SetConnectionID(cfg.ConnectionID)
// <> means unset. This is to differentiate from empty value. wc.SetActiveChannelID("")
// Because we need to support mobile clients where the value might be unset. wc.SetActiveTeamID("")
wc.SetActiveChannelID(UnsetPresenceIndicator) wc.SetActiveRHSThreadChannelID("")
wc.SetActiveTeamID(UnsetPresenceIndicator) wc.SetActiveThreadViewThreadChannelID("")
wc.SetActiveRHSThreadChannelID(UnsetPresenceIndicator)
wc.SetActiveThreadViewThreadChannelID(UnsetPresenceIndicator)
ps.Go(func() { ps.Go(func() {
runner.RunMultiHook(func(hooks plugin.Hooks) bool { runner.RunMultiHook(func(hooks plugin.Hooks) bool {
@ -314,9 +305,6 @@ func (wc *WebConn) SetActiveChannelID(id string) {
// GetActiveChannelID returns the active channel id of the connection. // GetActiveChannelID returns the active channel id of the connection.
func (wc *WebConn) GetActiveChannelID() string { func (wc *WebConn) GetActiveChannelID() string {
if wc.activeChannelID.Load() == nil {
return UnsetPresenceIndicator
}
return wc.activeChannelID.Load().(string) return wc.activeChannelID.Load().(string)
} }
@ -327,17 +315,11 @@ func (wc *WebConn) SetActiveTeamID(id string) {
// GetActiveTeamID returns the active team id of the connection. // GetActiveTeamID returns the active team id of the connection.
func (wc *WebConn) GetActiveTeamID() string { func (wc *WebConn) GetActiveTeamID() string {
if wc.activeTeamID.Load() == nil {
return UnsetPresenceIndicator
}
return wc.activeTeamID.Load().(string) return wc.activeTeamID.Load().(string)
} }
// GetActiveRHSThreadChannelID returns the channel id of the active thread of the connection. // GetActiveRHSThreadChannelID returns the channel id of the active thread of the connection.
func (wc *WebConn) GetActiveRHSThreadChannelID() string { func (wc *WebConn) GetActiveRHSThreadChannelID() string {
if wc.activeRHSThreadChannelID.Load() == nil {
return UnsetPresenceIndicator
}
return wc.activeRHSThreadChannelID.Load().(string) return wc.activeRHSThreadChannelID.Load().(string)
} }
@ -348,9 +330,6 @@ func (wc *WebConn) SetActiveRHSThreadChannelID(id string) {
// GetActiveThreadViewThreadChannelID returns the channel id of the active thread of the connection. // GetActiveThreadViewThreadChannelID returns the channel id of the active thread of the connection.
func (wc *WebConn) GetActiveThreadViewThreadChannelID() string { func (wc *WebConn) GetActiveThreadViewThreadChannelID() string {
if wc.activeThreadViewThreadChannelID.Load() == nil {
return UnsetPresenceIndicator
}
return wc.activeThreadViewThreadChannelID.Load().(string) return wc.activeThreadViewThreadChannelID.Load().(string)
} }
@ -359,11 +338,6 @@ func (wc *WebConn) SetActiveThreadViewThreadChannelID(id string) {
wc.activeThreadViewThreadChannelID.Store(id) wc.activeThreadViewThreadChannelID.Store(id)
} }
// isSet is a helper to check if a value is unset or not.
func (wc *WebConn) isSet(val string) bool {
return val != UnsetPresenceIndicator
}
// areAllInactive returns whether all of the connections // areAllInactive returns whether all of the connections
// are inactive or not. // are inactive or not.
func areAllInactive(conns []*WebConn) bool { func areAllInactive(conns []*WebConn) bool {
@ -873,18 +847,7 @@ func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
} }
// Only report events to users who are in the channel for the event // Only report events to users who are in the channel for the event
if chID := msg.GetBroadcast().ChannelId; chID != "" { if msg.GetBroadcast().ChannelId != "" {
// For typing events, we don't send them to users who don't have
// that channel or thread opened.
if wc.Platform.Config().FeatureFlags.WebSocketEventScope &&
utils.Contains([]model.WebsocketEventType{
model.WebsocketEventTyping,
model.WebsocketEventReactionAdded,
model.WebsocketEventReactionRemoved,
}, msg.EventType()) && wc.notInChannel(chID) && wc.notInThread(chID) {
return false
}
if model.GetMillis()-wc.lastAllChannelMembersTime > webConnMemberCacheTime { if model.GetMillis()-wc.lastAllChannelMembersTime > webConnMemberCacheTime {
wc.allChannelMembers = nil wc.allChannelMembers = nil
wc.lastAllChannelMembersTime = 0 wc.lastAllChannelMembersTime = 0
@ -900,7 +863,7 @@ func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
wc.lastAllChannelMembersTime = model.GetMillis() wc.lastAllChannelMembersTime = model.GetMillis()
} }
if _, ok := wc.allChannelMembers[chID]; ok { if _, ok := wc.allChannelMembers[msg.GetBroadcast().ChannelId]; ok {
return true return true
} }
return false return false
@ -918,15 +881,6 @@ func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
return true return true
} }
func (wc *WebConn) notInChannel(val string) bool {
return (wc.isSet(wc.GetActiveChannelID()) && val != wc.GetActiveChannelID())
}
func (wc *WebConn) notInThread(val string) bool {
return (wc.isSet(wc.GetActiveRHSThreadChannelID()) && val != wc.GetActiveRHSThreadChannelID()) &&
(wc.isSet(wc.GetActiveThreadViewThreadChannelID()) && val != wc.GetActiveThreadViewThreadChannelID())
}
// IsMemberOfTeam returns whether the user of the WebConn // IsMemberOfTeam returns whether the user of the WebConn
// is a member of the given teamID or not. // is a member of the given teamID or not.
func (wc *WebConn) isMemberOfTeam(teamID string) bool { func (wc *WebConn) isMemberOfTeam(teamID string) bool {

View File

@ -4,7 +4,6 @@
package app package app
import ( import (
"os"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -16,8 +15,6 @@ import (
) )
func TestWebConnShouldSendEvent(t *testing.T) { func TestWebConnShouldSendEvent(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_WEBSOCKETEVENTSCOPE", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_WEBSOCKETEVENTSCOPE")
th := Setup(t).InitBasic() th := Setup(t).InitBasic()
defer th.TearDown() defer th.TearDown()
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Roles: th.BasicUser.GetRawRoles(), TeamMembers: []*model.TeamMember{ session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Roles: th.BasicUser.GetRawRoles(), TeamMembers: []*model.TeamMember{
@ -165,63 +162,6 @@ func TestWebConnShouldSendEvent(t *testing.T) {
assert.False(t, adminUserWc.ShouldSendEvent(event), "did not expect admin") assert.False(t, adminUserWc.ShouldSendEvent(event), "did not expect admin")
}) })
t.Run("should not send typing event unless in scope", func(t *testing.T) {
event2 := model.NewWebSocketEvent(model.WebsocketEventTyping, "", th.BasicChannel.Id, "", nil, "")
// Basic, unset case
basicUserWc.SetActiveChannelID(platform.UnsetPresenceIndicator)
basicUserWc.SetActiveRHSThreadChannelID(platform.UnsetPresenceIndicator)
basicUserWc.SetActiveThreadViewThreadChannelID(platform.UnsetPresenceIndicator)
assert.True(t, basicUserWc.ShouldSendEvent(event2))
// Active channel is set to something else, thread unset
basicUserWc.SetActiveChannelID("ch1")
basicUserWc.SetActiveRHSThreadChannelID(platform.UnsetPresenceIndicator)
basicUserWc.SetActiveThreadViewThreadChannelID(platform.UnsetPresenceIndicator)
assert.True(t, basicUserWc.ShouldSendEvent(event2))
// Active channel is unset, thread set
basicUserWc.SetActiveChannelID(platform.UnsetPresenceIndicator)
basicUserWc.SetActiveRHSThreadChannelID("ch1")
basicUserWc.SetActiveThreadViewThreadChannelID("ch2")
assert.True(t, basicUserWc.ShouldSendEvent(event2))
// both are set to correct channel
basicUserWc.SetActiveChannelID(th.BasicChannel.Id)
basicUserWc.SetActiveRHSThreadChannelID(th.BasicChannel.Id)
basicUserWc.SetActiveThreadViewThreadChannelID(th.BasicChannel.Id)
assert.True(t, basicUserWc.ShouldSendEvent(event2))
// channel is correct, thread is something else.
basicUserWc.SetActiveChannelID(th.BasicChannel.Id)
basicUserWc.SetActiveRHSThreadChannelID("ch1")
basicUserWc.SetActiveThreadViewThreadChannelID("ch2")
assert.True(t, basicUserWc.ShouldSendEvent(event2))
// channel is wrong, thread is correct.
basicUserWc.SetActiveChannelID("ch1")
basicUserWc.SetActiveRHSThreadChannelID(th.BasicChannel.Id)
basicUserWc.SetActiveThreadViewThreadChannelID(th.BasicChannel.Id)
assert.True(t, basicUserWc.ShouldSendEvent(event2))
// FINALLY, both are set to something else.
basicUserWc.SetActiveChannelID("ch1")
basicUserWc.SetActiveRHSThreadChannelID("ch1")
basicUserWc.SetActiveThreadViewThreadChannelID("ch2")
assert.False(t, basicUserWc.ShouldSendEvent(event2))
// Different threads and channel
basicUserWc.SetActiveChannelID("ch1")
basicUserWc.SetActiveRHSThreadChannelID("ch2")
basicUserWc.SetActiveThreadViewThreadChannelID("ch3")
assert.False(t, basicUserWc.ShouldSendEvent(event2))
// Other channel. Thread unset explicitly.
basicUserWc.SetActiveChannelID("ch1")
basicUserWc.SetActiveRHSThreadChannelID("")
basicUserWc.SetActiveThreadViewThreadChannelID("")
assert.False(t, basicUserWc.ShouldSendEvent(event2))
})
t.Run("should send to basic user and admin in channel2", func(t *testing.T) { t.Run("should send to basic user and admin in channel2", func(t *testing.T) {
event = event.SetBroadcast(&model.WebsocketBroadcast{ChannelId: channel2.Id}) event = event.SetBroadcast(&model.WebsocketBroadcast{ChannelId: channel2.Id})

View File

@ -49,9 +49,7 @@ type FeatureFlags struct {
CloudIPFiltering bool CloudIPFiltering bool
ConsumePostHook bool ConsumePostHook bool
CloudAnnualRenewals bool CloudAnnualRenewals bool
OutgoingOAuthConnections bool
WebSocketEventScope bool
} }
func (f *FeatureFlags) SetDefaults() { func (f *FeatureFlags) SetDefaults() {
@ -71,8 +69,6 @@ func (f *FeatureFlags) SetDefaults() {
f.CloudIPFiltering = false f.CloudIPFiltering = false
f.ConsumePostHook = false f.ConsumePostHook = false
f.CloudAnnualRenewals = false f.CloudAnnualRenewals = false
f.OutgoingOAuthConnections = false
f.WebSocketEventScope = false
} }
// ToMap returns the feature flags as a map[string]string // ToMap returns the feature flags as a map[string]string

View File

@ -11,6 +11,7 @@ import type {UserProfile} from '@mattermost/types/users';
import type {IDMappedObjects} from '@mattermost/types/utilities'; import type {IDMappedObjects} from '@mattermost/types/utilities';
import {SearchTypes} from 'mattermost-redux/action_types'; import {SearchTypes} from 'mattermost-redux/action_types';
import * as PostActions from 'mattermost-redux/actions/posts';
import * as SearchActions from 'mattermost-redux/actions/search'; import * as SearchActions from 'mattermost-redux/actions/search';
import {trackEvent} from 'actions/telemetry_actions.jsx'; import {trackEvent} from 'actions/telemetry_actions.jsx';
@ -170,6 +171,15 @@ describe('rhs view actions', () => {
root_id: 'root123', root_id: 'root123',
} as Post; } as Post;
test('it dispatches PostActions.getPostThread correctly', () => {
store.dispatch(selectPostFromRightHandSideSearch(post));
const compareStore = mockStore(initialState);
compareStore.dispatch(PostActions.getPostThread(post.root_id));
expect(store.getActions()[0]).toEqual(compareStore.getActions()[0]);
});
describe(`it dispatches ${ActionTypes.SELECT_POST} correctly`, () => { describe(`it dispatches ${ActionTypes.SELECT_POST} correctly`, () => {
it('with mocked date', async () => { it('with mocked date', async () => {
store = mockStore({ store = mockStore({
@ -192,7 +202,7 @@ describe('rhs view actions', () => {
timestamp: POST_CREATED_TIME, timestamp: POST_CREATED_TIME,
}; };
expect(store.getActions()[0]).toEqual(action); expect(store.getActions()[1]).toEqual(action);
}); });
}); });
}); });
@ -782,6 +792,7 @@ describe('rhs view actions', () => {
await store.dispatch(openAtPrevious({selectedPostId: previousSelectedPost.id, previousRhsState: previousState})); await store.dispatch(openAtPrevious({selectedPostId: previousSelectedPost.id, previousRhsState: previousState}));
const compareStore = mockStore(initialState); const compareStore = mockStore(initialState);
compareStore.dispatch(PostActions.getPostThread(previousSelectedPost.root_id));
compareStore.dispatch({ compareStore.dispatch({
type: ActionTypes.SELECT_POST, type: ActionTypes.SELECT_POST,
postId: previousSelectedPost.root_id, postId: previousSelectedPost.root_id,

View File

@ -9,6 +9,7 @@ import type {Post} from '@mattermost/types/posts';
import {SearchTypes} from 'mattermost-redux/action_types'; import {SearchTypes} from 'mattermost-redux/action_types';
import {getChannel} from 'mattermost-redux/actions/channels'; import {getChannel} from 'mattermost-redux/actions/channels';
import * as PostActions from 'mattermost-redux/actions/posts';
import {getPostsByIds, getPost as fetchPost} from 'mattermost-redux/actions/posts'; import {getPostsByIds, getPost as fetchPost} from 'mattermost-redux/actions/posts';
import { import {
clearSearch, clearSearch,
@ -39,6 +40,7 @@ import type {RhsState} from 'types/store/rhs';
function selectPostFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState): ActionFuncAsync<boolean, GlobalState> { function selectPostFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState): ActionFuncAsync<boolean, GlobalState> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const postRootId = post.root_id || post.id; const postRootId = post.root_id || post.id;
await dispatch(PostActions.getPostThread(postRootId));
const state = getState(); const state = getState();
dispatch({ dispatch({

View File

@ -13,7 +13,6 @@ exports[`components/channel_view Should match snapshot if channel is archived 1`
channelIsArchived={true} channelIsArchived={true}
deactivatedChannel={false} deactivatedChannel={false}
enableOnboardingFlow={true} enableOnboardingFlow={true}
enableWebSocketEventScope={false}
goToLastViewedChannel={[MockFunction]} goToLastViewedChannel={[MockFunction]}
history={Object {}} history={Object {}}
isCloud={false} isCloud={false}
@ -70,7 +69,6 @@ exports[`components/channel_view Should match snapshot if channel is deactivated
channelIsArchived={false} channelIsArchived={false}
deactivatedChannel={true} deactivatedChannel={true}
enableOnboardingFlow={true} enableOnboardingFlow={true}
enableWebSocketEventScope={false}
goToLastViewedChannel={[MockFunction]} goToLastViewedChannel={[MockFunction]}
history={Object {}} history={Object {}}
isCloud={false} isCloud={false}
@ -126,7 +124,6 @@ exports[`components/channel_view Should match snapshot with base props 1`] = `
channelIsArchived={false} channelIsArchived={false}
deactivatedChannel={false} deactivatedChannel={false}
enableOnboardingFlow={true} enableOnboardingFlow={true}
enableWebSocketEventScope={false}
goToLastViewedChannel={[MockFunction]} goToLastViewedChannel={[MockFunction]}
history={Object {}} history={Object {}}
isCloud={false} isCloud={false}

View File

@ -24,7 +24,6 @@ describe('components/channel_view', () => {
isCloud: false, isCloud: false,
goToLastViewedChannel: jest.fn(), goToLastViewedChannel: jest.fn(),
isFirstAdmin: false, isFirstAdmin: false,
enableWebSocketEventScope: false,
}; };
it('Should match snapshot with base props', () => { it('Should match snapshot with base props', () => {

View File

@ -89,7 +89,7 @@ export default class ChannelView extends React.PureComponent<Props, State> {
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
// TODO: debounce // TODO: debounce
if (prevProps.channelId !== this.props.channelId && this.props.enableWebSocketEventScope) { if (prevProps.channelId !== this.props.channelId) {
WebSocketClient.updateActiveChannel(this.props.channelId); WebSocketClient.updateActiveChannel(this.props.channelId);
} }
if (prevProps.channelId !== this.props.channelId || prevProps.channelIsArchived !== this.props.channelIsArchived) { if (prevProps.channelId !== this.props.channelId || prevProps.channelIsArchived !== this.props.channelIsArchived) {

View File

@ -29,7 +29,6 @@ function mapStateToProps(state: GlobalState) {
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true'; const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
const enableOnboardingFlow = config.EnableOnboardingFlow === 'true'; const enableOnboardingFlow = config.EnableOnboardingFlow === 'true';
const enableWebSocketEventScope = config.FeatureFlagWebSocketEventScope === 'true';
return { return {
channelId: channel ? channel.id : '', channelId: channel ? channel.id : '',
@ -40,7 +39,6 @@ function mapStateToProps(state: GlobalState) {
isCloud: getLicense(state).Cloud === 'true', isCloud: getLicense(state).Cloud === 'true',
teamUrl: getCurrentRelativeTeamUrl(state), teamUrl: getCurrentRelativeTeamUrl(state),
isFirstAdmin: isFirstAdmin(state), isFirstAdmin: isFirstAdmin(state),
enableWebSocketEventScope,
}; };
} }

View File

@ -38,7 +38,6 @@ function mapStateToProps(state: GlobalState) {
const products = state.plugins.components.Product || []; const products = state.plugins.components.Product || [];
const [unreadTeamsSet, mentionsInTeamMap, teamHasUrgentMap] = getTeamsUnreadStatuses(state); const [unreadTeamsSet, mentionsInTeamMap, teamHasUrgentMap] = getTeamsUnreadStatuses(state);
const enableWebSocketEventScope = config.FeatureFlagWebSocketEventScope === 'true';
return { return {
currentTeamId: getCurrentTeamId(state), currentTeamId: getCurrentTeamId(state),
@ -52,7 +51,6 @@ function mapStateToProps(state: GlobalState) {
unreadTeamsSet, unreadTeamsSet,
mentionsInTeamMap, mentionsInTeamMap,
teamHasUrgentMap, teamHasUrgentMap,
enableWebSocketEventScope,
}; };
} }

View File

@ -149,7 +149,7 @@ export default class TeamSidebar extends React.PureComponent<Props, State> {
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
// TODO: debounce // TODO: debounce
if (prevProps.currentTeamId !== this.props.currentTeamId && this.props.enableWebSocketEventScope) { if (prevProps.currentTeamId !== this.props.currentTeamId) {
WebSocketClient.updateActiveTeam(this.props.currentTeamId); WebSocketClient.updateActiveTeam(this.props.currentTeamId);
} }
} }

View File

@ -6,7 +6,6 @@ import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux'; import type {Dispatch} from 'redux';
import type {Channel} from '@mattermost/types/channels'; import type {Channel} from '@mattermost/types/channels';
import type {ClientConfig} from '@mattermost/types/config';
import type {UserThread} from '@mattermost/types/threads'; import type {UserThread} from '@mattermost/types/threads';
import {fetchRHSAppsBindings} from 'mattermost-redux/actions/apps'; import {fetchRHSAppsBindings} from 'mattermost-redux/actions/apps';
@ -14,7 +13,6 @@ import {getNewestPostThread, getPostThread} from 'mattermost-redux/actions/posts
import {getThread as fetchThread, updateThreadRead} from 'mattermost-redux/actions/threads'; import {getThread as fetchThread, updateThreadRead} from 'mattermost-redux/actions/threads';
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getPost, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts'; import {getPost, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
@ -45,8 +43,6 @@ function makeMapStateToProps() {
const socketStatus = getSocketStatus(state); const socketStatus = getSocketStatus(state);
const highlightedPostId = getHighlightedPostId(state); const highlightedPostId = getHighlightedPostId(state);
const selectedPostFocusedAt = getSelectedPostFocussedAt(state); const selectedPostFocusedAt = getSelectedPostFocussedAt(state);
const config: Partial<ClientConfig> = getConfig(state);
const enableWebSocketEventScope = config.FeatureFlagWebSocketEventScope === 'true';
let postIds: string[] = []; let postIds: string[] = [];
let userThread: UserThread | null = null; let userThread: UserThread | null = null;
@ -70,7 +66,6 @@ function makeMapStateToProps() {
channel, channel,
highlightedPostId, highlightedPostId,
selectedPostFocusedAt, selectedPostFocusedAt,
enableWebSocketEventScope,
}; };
}; };
} }

View File

@ -68,7 +68,6 @@ describe('components/threading/ThreadViewer', () => {
appsEnabled: true, appsEnabled: true,
rootPostId: post.id, rootPostId: post.id,
isThreadView: true, isThreadView: true,
enableWebSocketEventScope: false,
}; };
test('should match snapshot', async () => { test('should match snapshot', async () => {

View File

@ -53,7 +53,6 @@ export type Props = Attrs & {
inputPlaceholder?: string; inputPlaceholder?: string;
rootPostId: string; rootPostId: string;
fromSuppressed?: boolean; fromSuppressed?: boolean;
enableWebSocketEventScope: boolean;
}; };
type State = { type State = {
@ -82,9 +81,7 @@ export default class ThreadViewer extends React.PureComponent<Props, State> {
} }
public componentWillUnmount() { public componentWillUnmount() {
if (this.props.enableWebSocketEventScope) { WebSocketClient.updateActiveThread(this.props.isThreadView, '');
WebSocketClient.updateActiveThread(this.props.isThreadView, '');
}
} }
public componentDidUpdate(prevProps: Props) { public componentDidUpdate(prevProps: Props) {
@ -173,7 +170,11 @@ export default class ThreadViewer extends React.PureComponent<Props, State> {
// scrolls to either bottom or new messages line // scrolls to either bottom or new messages line
private onInit = async (reconnected = false): Promise<void> => { private onInit = async (reconnected = false): Promise<void> => {
this.setState({isLoading: !reconnected}); this.setState({isLoading: !reconnected});
await this.props.actions.getPostThread(this.props.selected?.id || this.props.rootPostId, !reconnected); if (reconnected || this.morePostsToFetch()) {
await this.props.actions.getPostThread(this.props.selected?.id || this.props.rootPostId, !reconnected);
} else {
await this.props.actions.getNewestPostThread(this.props.selected?.id || this.props.rootPostId);
}
if ( if (
this.props.isCollapsedThreadsEnabled && this.props.isCollapsedThreadsEnabled &&
@ -182,7 +183,7 @@ export default class ThreadViewer extends React.PureComponent<Props, State> {
await this.fetchThread(); await this.fetchThread();
} }
if (this.props.channel && this.props.enableWebSocketEventScope) { if (this.props.channel) {
WebSocketClient.updateActiveThread(this.props.isThreadView, this.props.channel?.id); WebSocketClient.updateActiveThread(this.props.isThreadView, this.props.channel?.id);
} }
this.setState({isLoading: false}); this.setState({isLoading: false});

View File

@ -41,8 +41,6 @@ export default keyMirror({
POST_DELETED: null, POST_DELETED: null,
POST_REMOVED: null, POST_REMOVED: null,
POST_PINNED_CHANGED: null,
RECEIVED_FOCUSED_POST: null, RECEIVED_FOCUSED_POST: null,
RECEIVED_EDIT_POST: null, RECEIVED_EDIT_POST: null,
RECEIVED_REACTION: null, RECEIVED_REACTION: null,

View File

@ -1131,115 +1131,87 @@ describe('Actions.Posts', () => {
expect(state.entities.teams.myMembers[teamId].mention_count).toBe(1); expect(state.entities.teams.myMembers[teamId].mention_count).toBe(1);
}); });
describe('pinPost', () => { it('pinPost', async () => {
test('should update post and channel stats', async () => { const {dispatch, getState} = store;
nock(Client4.getBaseRoute()).
get(`/channels/${TestHelper.basicChannel!.id}/stats?exclude_files_count=true`).
reply(200, {channel_id: TestHelper.basicChannel!.id, member_count: 1, pinnedpost_count: 0});
await store.dispatch(getChannelStats(TestHelper.basicChannel!.id));
const post = TestHelper.fakePostWithId(TestHelper.basicChannel!.id); nock(Client4.getBaseRoute()).
store.dispatch(Actions.receivedPost(post)); get(`/channels/${TestHelper.basicChannel!.id}/stats?exclude_files_count=true`).
reply(200, {channel_id: TestHelper.basicChannel!.id, member_count: 1, pinnedpost_count: 0});
nock(Client4.getBaseRoute()). await dispatch(getChannelStats(TestHelper.basicChannel!.id));
post(`/posts/${post.id}/pin`).
reply(200, OK_RESPONSE);
const result = await store.dispatch(Actions.pinPost(post.id)); nock(Client4.getBaseRoute()).
expect(result.error).toBeUndefined(); post('/posts').
reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel!.id));
const post1 = await Client4.createPost(
TestHelper.fakePost(TestHelper.basicChannel!.id),
);
const state = store.getState(); const postList = {order: [post1.id], posts: {}} as PostList;
expect(state.entities.posts.posts[post.id].is_pinned).toBe(true); postList.posts[post1.id] = post1;
expect(state.entities.channels.stats[TestHelper.basicChannel!.id].pinnedpost_count).toBe(1);
});
test('MM-14115 should not clobber reactions on pinned post', async () => { nock(Client4.getBaseRoute()).
const post = TestHelper.getPostMock({ get(`/posts/${post1.id}/thread?skipFetchThreads=false&collapsedThreads=true&collapsedThreadsExtended=false&direction=down&perPage=60`).
id: TestHelper.generateId(), reply(200, postList);
metadata: { await dispatch(Actions.getPostThread(post1.id));
embeds: [],
emojis: [],
files: [],
images: {},
reactions: [
TestHelper.getReactionMock({emoji_name: 'test'}),
],
},
});
store.dispatch(Actions.receivedPost(post)); nock(Client4.getBaseRoute()).
post(`/posts/${post1.id}/pin`).
reply(200, OK_RESPONSE);
await dispatch(Actions.pinPost(post1.id));
let state = store.getState(); const state = getState();
expect(state.entities.posts.posts[post.id].is_pinned).toBe(false); const {stats} = state.entities.channels;
expect(Object.keys(state.entities.posts.reactions[post.id])).toHaveLength(1); const post = state.entities.posts.posts[post1.id];
const pinnedPostCount = stats[TestHelper.basicChannel!.id].pinnedpost_count;
nock(Client4.getBaseRoute()). expect(post).toBeTruthy();
post(`/posts/${post.id}/pin`). expect(post.is_pinned === true).toBeTruthy();
reply(200, OK_RESPONSE); expect(pinnedPostCount === 1).toBeTruthy();
const result = await store.dispatch(Actions.pinPost(post.id));
expect(result.error).toBeUndefined();
state = store.getState();
expect(state.entities.posts.posts[post.id].is_pinned).toBe(true);
expect(Object.keys(state.entities.posts.reactions[post.id])).toHaveLength(1);
});
}); });
describe('unpinPost', () => { it('unpinPost', async () => {
test('should update post and channel stats', async () => { const {dispatch, getState} = store;
nock(Client4.getBaseRoute()).
get(`/channels/${TestHelper.basicChannel!.id}/stats?exclude_files_count=true`).
reply(200, {channel_id: TestHelper.basicChannel!.id, member_count: 1, pinnedpost_count: 1});
await store.dispatch(getChannelStats(TestHelper.basicChannel!.id));
const post = TestHelper.fakePostWithId(TestHelper.basicChannel!.id); nock(Client4.getBaseRoute()).
store.dispatch(Actions.receivedPost(post)); get(`/channels/${TestHelper.basicChannel!.id}/stats?exclude_files_count=true`).
reply(200, {channel_id: TestHelper.basicChannel!.id, member_count: 1, pinnedpost_count: 0});
nock(Client4.getBaseRoute()). await dispatch(getChannelStats(TestHelper.basicChannel!.id));
post(`/posts/${post.id}/unpin`).
reply(200, OK_RESPONSE);
const result = await store.dispatch(Actions.unpinPost(post.id)); nock(Client4.getBaseRoute()).
expect(result.error).toBeUndefined(); post('/posts').
reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel!.id));
const post1 = await Client4.createPost(
TestHelper.fakePost(TestHelper.basicChannel!.id),
);
const state = store.getState(); const postList = {order: [post1.id], posts: {}} as PostList;
expect(state.entities.posts.posts[post.id].is_pinned).toBe(false); postList.posts[post1.id] = post1;
expect(state.entities.channels.stats[TestHelper.basicChannel!.id].pinnedpost_count).toBe(0);
});
test('MM-14115 should not clobber reactions on pinned post', async () => { nock(Client4.getBaseRoute()).
const post = TestHelper.getPostMock({ get(`/posts/${post1.id}/thread?skipFetchThreads=false&collapsedThreads=true&collapsedThreadsExtended=false&direction=down&perPage=60`).
id: TestHelper.generateId(), reply(200, postList);
is_pinned: true, await dispatch(Actions.getPostThread(post1.id));
metadata: {
embeds: [],
emojis: [],
files: [],
images: {},
reactions: [
TestHelper.getReactionMock({emoji_name: 'test'}),
],
},
});
store.dispatch(Actions.receivedPost(post)); nock(Client4.getBaseRoute()).
post(`/posts/${post1.id}/pin`).
reply(200, OK_RESPONSE);
await dispatch(Actions.pinPost(post1.id));
let state = store.getState(); nock(Client4.getBaseRoute()).
expect(state.entities.posts.posts[post.id].is_pinned).toBe(true); post(`/posts/${post1.id}/unpin`).
expect(Object.keys(state.entities.posts.reactions[post.id])).toHaveLength(1); reply(200, OK_RESPONSE);
await dispatch(Actions.unpinPost(post1.id));
nock(Client4.getBaseRoute()). const state = getState();
post(`/posts/${post.id}/unpin`). const {stats} = state.entities.channels;
reply(200, OK_RESPONSE); const post = state.entities.posts.posts[post1.id];
const pinnedPostCount = stats[TestHelper.basicChannel!.id].pinnedpost_count;
const result = await store.dispatch(Actions.unpinPost(post.id)); expect(post).toBeTruthy();
expect(result.error).toBeUndefined(); expect(post.is_pinned === false).toBeTruthy();
expect(pinnedPostCount === 0).toBeTruthy();
state = store.getState();
expect(state.entities.posts.posts[post.id].is_pinned).toBe(false);
expect(Object.keys(state.entities.posts.reactions[post.id])).toHaveLength(1);
});
}); });
it('addReaction', async () => { it('addReaction', async () => {

View File

@ -137,15 +137,6 @@ export function postRemoved(post: Post) {
}; };
} }
export function postPinnedChanged(postId: string, isPinned: boolean, updateAt = Date.now()) {
return {
type: PostTypes.POST_PINNED_CHANGED,
postId,
isPinned,
updateAt,
};
}
export function getPost(postId: string): ActionFuncAsync<Post> { export function getPost(postId: string): ActionFuncAsync<Post> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
let post; let post;
@ -534,7 +525,11 @@ export function pinPost(postId: string): ActionFuncAsync {
const post = PostSelectors.getPost(state, postId); const post = PostSelectors.getPost(state, postId);
if (post) { if (post) {
actions.push( actions.push(
postPinnedChanged(postId, true, Date.now()), receivedPost({
...post,
is_pinned: true,
update_at: Date.now(),
}, isCollapsedThreadsEnabled(state)),
{ {
type: ChannelTypes.INCREMENT_PINNED_POST_COUNT, type: ChannelTypes.INCREMENT_PINNED_POST_COUNT,
id: post.channel_id, id: post.channel_id,
@ -582,7 +577,11 @@ export function unpinPost(postId: string): ActionFuncAsync {
const post = PostSelectors.getPost(state, postId); const post = PostSelectors.getPost(state, postId);
if (post) { if (post) {
actions.push( actions.push(
postPinnedChanged(postId, false, Date.now()), receivedPost({
...post,
is_pinned: false,
update_at: Date.now(),
}, isCollapsedThreadsEnabled(state)),
decrementPinnedPostCount(post.channel_id), decrementPinnedPostCount(post.channel_id),
); );
} }

View File

@ -3615,116 +3615,79 @@ describe('reactions', () => {
PostTypes.RECEIVED_POST, PostTypes.RECEIVED_POST,
]) { ]) {
describe(`single post received (${actionType})`, () => { describe(`single post received (${actionType})`, () => {
it('should not store anything for a post first received without metadata', () => { it('no post metadata', () => {
// This shouldn't occur based on our type definitions, but it is possible
const post = TestHelper.getPostMock({
id: 'post',
});
(post as any).metadata = undefined;
const state = deepFreeze({}); const state = deepFreeze({});
const action = { const action = {
type: actionType, type: actionType,
data: post, data: {
}; id: 'post',
const nextState = reducers.reactions(state, action);
expect(nextState).toBe(state);
});
it('should not change stored state for a post received without metadata', () => {
// This shouldn't occur based on our type definitions, but it is possible
const post = TestHelper.getPostMock({
id: 'post',
});
(post as any).metadata = undefined;
const state = deepFreeze({
post: {
'user-taco': TestHelper.getReactionMock({user_id: 'user', emoji_name: 'taco'}),
}, },
});
const action = {
type: actionType,
data: post,
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
expect(nextState).toBe(state); expect(nextState).toEqual(state);
}); });
it('should store when a post is first received without reactions', () => { it('no reactions in post metadata', () => {
const post = TestHelper.getPostMock({
id: 'post',
});
post.metadata.reactions = undefined;
const state = deepFreeze({}); const state = deepFreeze({});
const action = { const action = {
type: actionType, type: actionType,
data: post, data: {
id: 'post',
metadata: {reactions: []},
},
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
expect(nextState).not.toBe(state); expect(nextState).not.toEqual(state);
expect(nextState).toEqual({ expect(nextState).toEqual({
post: {}, post: {},
}); });
}); });
it('should remove existing reactions when a post is received without reactions', () => { it('should not clobber reactions when metadata empty', () => {
const post = TestHelper.getPostMock({ const state = deepFreeze({post: {name: 'smiley', post_id: 'post'}});
id: 'post',
});
post.metadata.reactions = undefined;
const state = deepFreeze({
post: {
'user-taco': TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '+1'}),
},
});
const action = { const action = {
type: actionType, type: actionType,
data: post, data: {
id: 'post',
metadata: {},
},
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
expect(nextState).not.toBe(state);
expect(nextState).toEqual({ expect(nextState).toEqual({
post: {}, post: {name: 'smiley', post_id: 'post'},
}); });
}); });
it('should save reactions', () => { it('should save reactions', () => {
const reactions = [
TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '+1'}),
TestHelper.getReactionMock({user_id: 'efgh', emoji_name: '+1'}),
TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '-1'}),
];
const state = deepFreeze({}); const state = deepFreeze({});
const action = { const action = {
type: actionType, type: actionType,
data: TestHelper.getPostMock({ data: {
id: 'post', id: 'post',
metadata: { metadata: {
reactions, reactions: [
{user_id: 'abcd', emoji_name: '+1'},
{user_id: 'efgh', emoji_name: '+1'},
{user_id: 'abcd', emoji_name: '-1'},
],
}, },
}), },
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
expect(nextState).not.toBe(state); expect(nextState).not.toEqual(state);
expect(nextState).toEqual({ expect(nextState).toEqual({
post: { post: {
'abcd-+1': reactions[0], 'abcd-+1': {user_id: 'abcd', emoji_name: '+1'},
'efgh-+1': reactions[1], 'efgh-+1': {user_id: 'efgh', emoji_name: '+1'},
'abcd--1': reactions[2], 'abcd--1': {user_id: 'abcd', emoji_name: '-1'},
}, },
}); });
}); });
@ -3733,10 +3696,10 @@ describe('reactions', () => {
const state = deepFreeze({}); const state = deepFreeze({});
const action = { const action = {
type: actionType, type: actionType,
data: TestHelper.getPostMock({ data: {
id: 'post', id: 'post',
delete_at: 1571366424287, delete_at: '1571366424287',
}), },
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
@ -3747,218 +3710,150 @@ describe('reactions', () => {
} }
describe('receiving multiple posts', () => { describe('receiving multiple posts', () => {
it('should not store anything for a post first received without metadata', () => { it('no post metadata', () => {
// This shouldn't occur based on our type definitions, but it is possible
const post = TestHelper.getPostMock({
id: 'post',
});
(post as any).metadata = undefined;
const state = deepFreeze({}); const state = deepFreeze({});
const action = { const action = {
type: PostTypes.RECEIVED_POSTS, type: PostTypes.RECEIVED_POSTS,
data: { data: {
posts: { posts: {
post, post: {
id: 'post',
},
}, },
}, },
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
expect(state).toBe(nextState); expect(nextState).toEqual(state);
}); });
it('should not change stored state for a post received without metadata', () => { it('no reactions in post metadata', () => {
// This shouldn't occur based on our type definitions, but it is possible
const post = TestHelper.getPostMock({
id: 'post',
});
(post as any).metadata = undefined;
const state = deepFreeze({
post: {
'user-taco': TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '+1'}),
},
});
const action = {
type: PostTypes.RECEIVED_POSTS,
data: {
posts: {
post,
},
},
};
const nextState = reducers.reactions(state, action);
expect(state).toBe(nextState);
});
it('should store when a post is first received without reactions', () => {
const post = TestHelper.getPostMock({
id: 'post',
});
post.metadata.reactions = undefined;
const state = deepFreeze({}); const state = deepFreeze({});
const action = { const action = {
type: PostTypes.RECEIVED_POSTS, type: PostTypes.RECEIVED_POSTS,
data: { data: {
posts: { posts: {
post, post: {
id: 'post',
metadata: {reactions: []},
},
}, },
}, },
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
expect(nextState).not.toBe(state); expect(nextState).not.toEqual(state);
expect(nextState).toEqual({
post: {},
});
});
it('should remove existing reactions when a post is received without reactions', () => {
const post = TestHelper.getPostMock({
id: 'post',
});
post.metadata.reactions = undefined;
const state = deepFreeze({
post: {
'user-taco': TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '+1'}),
},
});
const action = {
type: PostTypes.RECEIVED_POSTS,
data: {
posts: {
post,
},
},
};
const nextState = reducers.reactions(state, action);
expect(nextState).not.toBe(state);
expect(nextState).toEqual({ expect(nextState).toEqual({
post: {}, post: {},
}); });
}); });
it('should save reactions', () => { it('should save reactions', () => {
const reactions = [
TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '+1'}),
TestHelper.getReactionMock({user_id: 'efgh', emoji_name: '+1'}),
TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '-1'}),
];
const state = deepFreeze({}); const state = deepFreeze({});
const action = { const action = {
type: PostTypes.RECEIVED_POSTS, type: PostTypes.RECEIVED_POSTS,
data: { data: {
posts: { posts: {
post: TestHelper.getPostMock({ post: {
id: 'post', id: 'post',
metadata: { metadata: {
reactions, reactions: [
{user_id: 'abcd', emoji_name: '+1'},
{user_id: 'efgh', emoji_name: '+1'},
{user_id: 'abcd', emoji_name: '-1'},
],
}, },
}), },
}, },
}, },
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
expect(nextState).not.toBe(state); expect(nextState).not.toEqual(state);
expect(nextState).toEqual({ expect(nextState).toEqual({
post: { post: {
'abcd-+1': reactions[0], 'abcd-+1': {user_id: 'abcd', emoji_name: '+1'},
'efgh-+1': reactions[1], 'efgh-+1': {user_id: 'efgh', emoji_name: '+1'},
'abcd--1': reactions[2], 'abcd--1': {user_id: 'abcd', emoji_name: '-1'},
}, },
}); });
}); });
it('should save reactions for multiple posts', () => { it('should save reactions for multiple posts', () => {
const reaction1 = TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '+1'});
const reaction2 = TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '-1'});
const state = deepFreeze({}); const state = deepFreeze({});
const action = { const action = {
type: PostTypes.RECEIVED_POSTS, type: PostTypes.RECEIVED_POSTS,
data: { data: {
posts: { posts: {
post1: TestHelper.getPostMock({ post1: {
id: 'post1', id: 'post1',
metadata: { metadata: {
reactions: [ reactions: [
reaction1, {user_id: 'abcd', emoji_name: '+1'},
], ],
}, },
}), },
post2: TestHelper.getPostMock({ post2: {
id: 'post2', id: 'post2',
metadata: { metadata: {
reactions: [ reactions: [
reaction2, {user_id: 'abcd', emoji_name: '-1'},
], ],
}, },
}), },
}, },
}, },
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
expect(nextState).not.toBe(state); expect(nextState).not.toEqual(state);
expect(nextState).toEqual({ expect(nextState).toEqual({
post1: { post1: {
'abcd-+1': reaction1, 'abcd-+1': {user_id: 'abcd', emoji_name: '+1'},
}, },
post2: { post2: {
'abcd--1': reaction2, 'abcd--1': {user_id: 'abcd', emoji_name: '-1'},
}, },
}); });
}); });
it('should save reactions for multiple posts except deleted posts', () => { it('should save reactions for multiple posts except deleted posts', () => {
const reaction1 = TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '+1'});
const reaction2 = TestHelper.getReactionMock({user_id: 'abcd', emoji_name: '-1'});
const state = deepFreeze({}); const state = deepFreeze({});
const action = { const action = {
type: PostTypes.RECEIVED_POSTS, type: PostTypes.RECEIVED_POSTS,
data: { data: {
posts: { posts: {
post1: TestHelper.getPostMock({ post1: {
id: 'post1', id: 'post1',
metadata: { metadata: {
reactions: [ reactions: [
reaction1, {user_id: 'abcd', emoji_name: '+1'},
], ],
}, },
}), },
post2: TestHelper.getPostMock({ post2: {
id: 'post2', id: 'post2',
delete_at: 1571366424287, delete_at: '1571366424287',
metadata: { metadata: {
reactions: [ reactions: [
reaction2, {user_id: 'abcd', emoji_name: '-1'},
], ],
}, },
}), },
}, },
}, },
}; };
const nextState = reducers.reactions(state, action); const nextState = reducers.reactions(state, action);
expect(nextState).not.toBe(state); expect(nextState).not.toEqual(state);
expect(nextState).toEqual({ expect(nextState).toEqual({
post1: { post1: {
'abcd-+1': reaction1, 'abcd-+1': {user_id: 'abcd', emoji_name: '+1'},
}, },
}); });
}); });

View File

@ -158,7 +158,7 @@ export function nextPostsReplies(state: {[x in Post['id']]: number} = {}, action
} }
} }
export function handlePosts(state: IDMappedObjects<Post> = {}, action: AnyAction) { export function handlePosts(state: RelationOneToOne<Post, Post> = {}, action: AnyAction) {
switch (action.type) { switch (action.type) {
case PostTypes.RECEIVED_POST: case PostTypes.RECEIVED_POST:
case PostTypes.RECEIVED_NEW_POST: { case PostTypes.RECEIVED_NEW_POST: {
@ -263,23 +263,6 @@ export function handlePosts(state: IDMappedObjects<Post> = {}, action: AnyAction
return nextState; return nextState;
} }
case PostTypes.POST_PINNED_CHANGED: {
const {postId, isPinned, updateAt} = action;
if (!state[postId]) {
return state;
}
return {
...state,
[postId]: {
...state[postId],
is_pinned: isPinned,
last_update_at: updateAt,
},
};
}
case ChannelTypes.RECEIVED_CHANNEL_DELETED: case ChannelTypes.RECEIVED_CHANNEL_DELETED:
case ChannelTypes.DELETE_CHANNEL_SUCCESS: case ChannelTypes.DELETE_CHANNEL_SUCCESS:
case ChannelTypes.LEAVE_CHANNEL: { case ChannelTypes.LEAVE_CHANNEL: {
@ -1307,8 +1290,8 @@ export function acknowledgements(state: RelationOneToOne<Post, Record<UserProfil
} }
} }
function storeReactionsForPost(state: RelationOneToOne<Post, Record<string, Reaction>>, post: Post) { function storeReactionsForPost(state: any, post: Post) {
if (!post.metadata || post.delete_at > 0) { if (!post.metadata || !post.metadata.reactions || post.delete_at > 0) {
return state; return state;
} }

View File

@ -12,7 +12,6 @@ import type {FileInfo} from '@mattermost/types/files';
import type {Group} from '@mattermost/types/groups'; import type {Group} from '@mattermost/types/groups';
import type {Command, DialogElement, OAuthApp} from '@mattermost/types/integrations'; import type {Command, DialogElement, OAuthApp} from '@mattermost/types/integrations';
import type {Post, PostMetadata} from '@mattermost/types/posts'; import type {Post, PostMetadata} from '@mattermost/types/posts';
import type {Reaction} from '@mattermost/types/reactions';
import type {Role} from '@mattermost/types/roles'; import type {Role} from '@mattermost/types/roles';
import type {Scheme} from '@mattermost/types/schemes'; import type {Scheme} from '@mattermost/types/schemes';
import type {Team, TeamMembership} from '@mattermost/types/teams'; import type {Team, TeamMembership} from '@mattermost/types/teams';
@ -724,16 +723,6 @@ class TestHelper {
}; };
} }
getReactionMock(override: Partial<Reaction> = {}): Reaction {
return {
user_id: '',
post_id: '',
emoji_name: '',
create_at: 0,
...override,
};
}
mockLogin = () => { mockLogin = () => {
const clientBaseRoute = this.basicClient4!.getBaseRoute(); const clientBaseRoute = this.basicClient4!.getBaseRoute();
nock(clientBaseRoute). nock(clientBaseRoute).

View File

@ -319,7 +319,7 @@ export class TestHelper {
return Object.assign({}, defaultOutgoingWebhook, override); return Object.assign({}, defaultOutgoingWebhook, override);
} }
public static getPostMock(override: Omit<Partial<Post>, 'metadata'> & {metadata?: Partial<Post['metadata']>} = {}): Post { public static getPostMock(override: Partial<Post> = {}): Post {
const defaultPost: Post = { const defaultPost: Post = {
edit_at: 0, edit_at: 0,
original_id: '', original_id: '',
@ -345,15 +345,7 @@ export class TestHelper {
update_at: 0, update_at: 0,
user_id: 'user_id', user_id: 'user_id',
}; };
return Object.assign({}, defaultPost, override);
return {
...defaultPost,
...override,
metadata: {
...defaultPost.metadata,
...override.metadata,
},
};
} }
public static getFileInfoMock(override: Partial<FileInfo> = {}): FileInfo { public static getFileInfoMock(override: Partial<FileInfo> = {}): FileInfo {

View File

@ -121,7 +121,6 @@ export type ClientConfig = {
FileLevel: string; FileLevel: string;
FeatureFlagAppsEnabled: string; FeatureFlagAppsEnabled: string;
FeatureFlagCallsEnabled: string; FeatureFlagCallsEnabled: string;
FeatureFlagWebSocketEventScope: string;
ForgotPasswordLink: string; ForgotPasswordLink: string;
GiphySdkKey: string; GiphySdkKey: string;
GoogleDeveloperKey: string; GoogleDeveloperKey: string;

View File

@ -66,7 +66,7 @@ export type PostMetadata = {
emojis: CustomEmoji[]; emojis: CustomEmoji[];
files: FileInfo[]; files: FileInfo[];
images: Record<string, PostImage>; images: Record<string, PostImage>;
reactions?: Reaction[]; reactions: Reaction[];
priority?: PostPriorityMetadata; priority?: PostPriorityMetadata;
acknowledgements?: PostAcknowledgement[]; acknowledgements?: PostAcknowledgement[];
}; };