Skip to content

Commit

Permalink
Merge pull request #670 from GetStream/vishal/deleted-user-handling
Browse files Browse the repository at this point in the history
CRNS-321: Update message references for deleted or updated users
  • Loading branch information
vishalnarkhede authored Apr 29, 2021
2 parents 639a91c + c02c856 commit 6eddf39
Show file tree
Hide file tree
Showing 5 changed files with 370 additions and 36 deletions.
131 changes: 131 additions & 0 deletions src/channel_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,17 @@ export class ChannelState<
for (let i = 0; i < newMessages.length; i += 1) {
const message = this.formatMessage(newMessages[i]);

if (message.user && this._channel?.cid) {
/**
* Store the reference to user for this channel, so that when we have to
* handle updates to user, we can use the reference map, to determine which
* channels need to be updated with updated user object.
*/
this._channel
.getClient()
.state.updateUserReference(message.user, this._channel.cid);
}

if (initializing && message.id && this.threads[message.id]) {
// If we are initializing the state of channel (e.g., in case of connection recovery),
// then in that case we remove thread related to this message from threads object.
Expand Down Expand Up @@ -603,6 +614,126 @@ export class ChannelState<

return { removed: result.length < msgArray.length, result };
};

/**
* Updates the message.user property with updated user object, for messages.
*
* @param {UserResponse<UserType>} user
*/
updateUserMessages = (user: UserResponse<UserType>) => {
const _updateUserMessages = (
messages: Array<
ReturnType<
ChannelState<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>['formatMessage']
>
>,
user: UserResponse<UserType>,
) => {
for (let i = 0; i < messages.length; i++) {
const m = messages[i];
if (m.user?.id === user.id) {
messages[i] = { ...m, user };
}
}
};

_updateUserMessages(this.messages, user);

for (const parentId in this.threads) {
_updateUserMessages(this.threads[parentId], user);
}

_updateUserMessages(this.pinnedMessages, user);
};

/**
* Marks the messages as deleted, from deleted user.
*
* @param {UserResponse<UserType>} user
* @param {boolean} hardDelete
*/
deleteUserMessages = (user: UserResponse<UserType>, hardDelete = false) => {
const _deleteUserMessages = (
messages: Array<
ReturnType<
ChannelState<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>['formatMessage']
>
>,
user: UserResponse<UserType>,
hardDelete = false,
) => {
for (let i = 0; i < messages.length; i++) {
const m = messages[i];
if (m.user?.id !== user.id) {
continue;
}

if (hardDelete) {
/**
* In case of hard delete, we need to strip down all text, html,
* attachments and all the custom properties on message
*/
messages[i] = ({
cid: m.cid,
created_at: m.created_at,
deleted_at: user.deleted_at,
id: m.id,
latest_reactions: [],
mentioned_users: [],
own_reactions: [],
parent_id: m.parent_id,
reply_count: m.reply_count,
status: m.status,
thread_participants: m.thread_participants,
type: 'deleted',
updated_at: m.updated_at,
user: m.user,
} as unknown) as ReturnType<
ChannelState<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>['formatMessage']
>;
} else {
messages[i] = {
...m,
type: 'deleted',
deleted_at: user.deleted_at,
};
}
}
};

_deleteUserMessages(this.messages, user, hardDelete);

for (const parentId in this.threads) {
_deleteUserMessages(this.threads[parentId], user, hardDelete);
}

_deleteUserMessages(this.pinnedMessages, user, hardDelete);
};

/**
* filterErrorMessages - Removes error messages from the channel state.
*
Expand Down
165 changes: 129 additions & 36 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,130 @@ export class StreamChat<
this.dispatchEvent(event);
};

/**
* Updates the members and watchers of the currently active channels that contain this user
*
* @param {UserResponse<UserType>} user
*/
_updateMemberWatcherReferences = (user: UserResponse<UserType>) => {
const refMap = this.state.userChannelReferences[user.id] || {};
for (const channelID in refMap) {
const channel = this.activeChannels[channelID];
/** search the members and watchers and update as needed... */
if (channel?.state) {
if (channel.state.members[user.id]) {
channel.state.members[user.id].user = user;
}
if (channel.state.watchers[user.id]) {
channel.state.watchers[user.id] = user;
}
}
}
};

/**
* @deprecated Please _updateMemberWatcherReferences instead.
* @private
*/
_updateUserReferences = this._updateMemberWatcherReferences;

/**
* @private
*
* Updates the messages from the currently active channels that contain this user,
* with updated user object.
*
* @param {UserResponse<UserType>} user
*/
_updateUserMessageReferences = (user: UserResponse<UserType>) => {
const refMap = this.state.userChannelReferences[user.id] || {};

for (const channelID in refMap) {
const channel = this.activeChannels[channelID];
const state = channel.state;

/** update the messages from this user. */
state?.updateUserMessages(user);
}
};

/**
* @private
*
* Deletes the messages from the currently active channels that contain this user
*
* If hardDelete is true, all the content of message will be stripped down.
* Otherwise, only 'message.type' will be set as 'deleted'.
*
* @param {UserResponse<UserType>} user
* @param {boolean} hardDelete
*/
_deleteUserMessageReference = (user: UserResponse<UserType>, hardDelete = false) => {
const refMap = this.state.userChannelReferences[user.id] || {};

for (const channelID in refMap) {
const channel = this.activeChannels[channelID];
const state = channel.state;

/** deleted the messages from this user. */
state?.deleteUserMessages(user, hardDelete);
}
};

/**
* @private
*
* Handle following user related events:
* - user.presence.changed
* - user.updated
* - user.deleted
*
* @param {Event} event
*/
_handleUserEvent = (
event: Event<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>,
) => {
if (!event.user) {
return;
}

/** update the client.state with any changes to users */
if (event.type === 'user.presence.changed' || event.type === 'user.updated') {
if (event.user?.id === this.userID) {
this.user = this.user && { ...this.user, ...event.user };
/** Updating only available properties in _user object. */
Object.keys(event.user).forEach((key) => {
if (this._user && key in this._user) {
/** @ts-expect-error */
this._user[key] = event.user[key];
}
});
}
this.state.updateUser(event.user);
this._updateMemberWatcherReferences(event.user);
}

if (event.type === 'user.updated') {
this._updateUserMessageReferences(event.user);
}

if (
event.type === 'user.deleted' &&
event.user.deleted_at &&
(event.mark_messages_deleted || event.hard_delete)
) {
this._deleteUserMessageReference(event.user, event.hard_delete);
}
};

_handleClientEvent(
event: Event<
AttachmentType,
Expand All @@ -1190,24 +1314,14 @@ export class StreamChat<
},
);

// update the client.state with any changes to users
if (
event.user &&
(event.type === 'user.presence.changed' || event.type === 'user.updated')
event.type === 'user.presence.changed' ||
event.type === 'user.updated' ||
event.type === 'user.deleted'
) {
if (event.user?.id === this.userID) {
this.user = this.user && { ...this.user, ...event.user };
// Updating only available properties in _user object.
Object.keys(event.user).forEach(function (key) {
if (client._user && key in client._user) {
// @ts-expect-error
client._user[key] = event.user[key];
}
});
}
client.state.updateUser(event.user);
client._updateUserReferences(event.user);
this._handleUserEvent(event);
}

if (event.type === 'health.check' && event.me) {
client.user = event.me;
client.state.updateUser(event.me);
Expand Down Expand Up @@ -1334,27 +1448,6 @@ export class StreamChat<
this.setUserPromise = Promise.resolve();
};

/*
_updateUserReferences updates the members and watchers of the currently active channels
that contain this user
*/
_updateUserReferences(user: UserResponse<UserType>) {
const refMap = this.state.userChannelReferences[user.id] || {};
const refs = Object.keys(refMap);
for (const channelID of refs) {
const channel = this.activeChannels[channelID];
// search the members and watchers and update as needed...
if (channel?.state) {
if (channel.state.members[user.id]) {
channel.state.members[user.id].user = user;
}
if (channel.state.watchers[user.id]) {
channel.state.watchers[user.id] = user;
}
}
}
}

/**
* @private
*/
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,8 @@ export type Event<
clear_history?: boolean;
connection_id?: string;
created_at?: string;
hard_delete?: boolean;
mark_messages_deleted?: boolean;
me?: OwnUserResponse<ChannelType, CommandType, UserType>;
member?: ChannelMemberResponse<UserType>;
message?: MessageResponse<
Expand Down
3 changes: 3 additions & 0 deletions test/unit/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getOrCreateChannelApi } from './test-utils/getOrCreateChannelApi';

import { StreamChat } from '../../src/client';
import { Channel } from '../../src/channel';
import { ClientState } from '../../src';

const expect = chai.expect;

Expand All @@ -23,6 +24,7 @@ describe('Channel count unread', function () {
user,
userID: 'user',
userMuteStatus: (targetId) => targetId.startsWith('mute'),
state: new ClientState(),
});

const ignoredMessages = [
Expand Down Expand Up @@ -114,6 +116,7 @@ describe('Channel _handleChannelEvent', function () {
user,
userID: user.id,
userMuteStatus: (targetId) => targetId.startsWith('mute'),
state: new ClientState(),
},
'messaging',
'id',
Expand Down
Loading

0 comments on commit 6eddf39

Please sign in to comment.