Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for member based channel #586

Merged
merged 24 commits into from
Jan 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
32cc933
Merge branch 'master' of github.com:GetStream/stream-chat-js into vis…
vishalnarkhede Jan 4, 2021
5f16699
Ensure unique member based channel on client
vishalnarkhede Jan 15, 2021
38bfcb9
Merge branch 'master' into vishal/thread_participants_type
vishalnarkhede Jan 15, 2021
8852374
Adding test
vishalnarkhede Jan 15, 2021
30841cd
Merge branch 'vishal/thread_participants_type' of github.com:GetStrea…
vishalnarkhede Jan 15, 2021
506d6be
Updated the test
vishalnarkhede Jan 15, 2021
40bc02c
Merge branch 'master' into vishal/thread_participants_type
Jan 15, 2021
de2ce01
Cover the edge cases of channel creation
vishalnarkhede Jan 18, 2021
3491ee4
Merge branch 'vishal/thread_participants_type' of github.com:GetStrea…
vishalnarkhede Jan 18, 2021
39d3db1
Adding channelType to temporary cid of unique conversation
vishalnarkhede Jan 18, 2021
877bb57
Fixing members related error
vishalnarkhede Jan 18, 2021
3abf6c9
Update test title
Jan 18, 2021
9065826
Fixing comments
vishalnarkhede Jan 18, 2021
5b6d6b9
Merge branch 'vishal/thread_participants_type' of github.com:GetStrea…
vishalnarkhede Jan 18, 2021
f8e9e72
Fixing comment grammer
vishalnarkhede Jan 18, 2021
763c4ab
Merge branch 'master' into vishal/thread_participants_type
vishalnarkhede Jan 18, 2021
3ff1be9
Merge branch 'master' into vishal/thread_participants_type
vishalnarkhede Jan 18, 2021
fcba1a6
Updated temporary cid for member based channels, to keep it consisten…
vishalnarkhede Jan 19, 2021
ba0d5c3
Merge github.com:GetStream/stream-chat-js into vishal/thread_particip…
vishalnarkhede Jan 19, 2021
21c2664
Merge branch 'vishal/thread_participants_type' of github.com:GetStrea…
vishalnarkhede Jan 19, 2021
f4cb2aa
Fixing code comment
vishalnarkhede Jan 19, 2021
f24efb7
Fixing tempCid cleanup from activeChannels
vishalnarkhede Jan 20, 2021
4ef8f28
Fixing memberStr during channel.query
vishalnarkhede Jan 20, 2021
9c7abc8
Moving test to unit-test
vishalnarkhede Jan 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,19 @@ export class Channel<
this.id = state.channel.id;
this.cid = state.channel.cid;
// set the channel as active...

const membersStr = state.members
.map((m) => m.user_id)
?.sort()
.join(',');
const tempChannelCid = `${this.type}:!members-${membersStr}`;

if (tempChannelCid in this.getClient().activeChannels) {
// This gets set in `client.channel()` function, when channel is created
// using members, not id.
delete this.getClient().activeChannels[tempChannelCid];
}

if (!(this.cid in this.getClient().activeChannels)) {
this.getClient().activeChannels[this.cid] = this;
}
Expand Down
110 changes: 94 additions & 16 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1513,27 +1513,105 @@ export class StreamChat<
UserType
>(this, channelType, undefined, custom);
}

// support channel("messaging", {options})
if (typeof channelIDOrCustom === 'object') {
return new Channel<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>(this, channelType, undefined, channelIDOrCustom);
return this.getChannelByMembers(channelType, channelIDOrCustom);
}

if (typeof channelIDOrCustom === 'string' && ~channelIDOrCustom.indexOf(':')) {
throw Error(
`Invalid channel id ${channelIDOrCustom}, can't contain the : character`,
);
return this.getChannelById(channelType, channelIDOrCustom, custom);
}

/**
* It's a helper method for `client.channel()` method, used to create unique conversation or
* channel based on member list instead of id.
*
* If the channel already exists in `activeChannels` list, then we simply return it, since that
* means the same channel was already requested or created.
*
* Otherwise we create a new instance of Channel class and return it.
*
* @private
*
* @param {string} channelType The channel type
* @param {object} [custom] Custom data to attach to the channel
*
* @return {channel} The channel object, initialize it using channel.watch()
*/
getChannelByMembers = (channelType: string, custom: ChannelData<ChannelType>) => {
// Check if the channel already exists.
// Only allow 1 channel object per cid
const membersStr = custom.members?.sort().join(',');
const tempCid = `${channelType}:!members-${membersStr}`;

if (!membersStr) {
throw Error('Please specify atleast one member when creating unique conversation');
}

// channel could exist in `activeChannels` list with either one of the following two keys:
// 1. cid - Which gets set on channel only after calling channel.query or channel.watch or channel.create
// 2. Sorted membersStr - E.g., "messaging:amin,vishal" OR "messaging:amin,jaap,tom"
// This is set when you create a channel, but haven't queried yet. After query,
// we will replace it with `cid`
for (const key in this.activeChannels) {
const channel = this.activeChannels[key];
if (key === tempCid) {
return channel;
}

if (key.indexOf(`${channelType}:!members-`) === 0) {
const membersStrInExistingChannel = Object.keys(channel.state.members)
.sort()
.join(',');
if (membersStrInExistingChannel === membersStr) {
return channel;
}
}
}

const channel = new Channel<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>(this, channelType, undefined, custom);

// For the time being set the key as membersStr, since we don't know the cid yet.
// In channel.query, we will replace it with 'cid'.
this.activeChannels[tempCid] = channel;
return channel;
};

/**
* Its a helper method for `client.channel()` method, used to channel given the id of channel.
*
* If the channel already exists in `activeChannels` list, then we simply return it, since that
* means the same channel was already requested or created.
*
* Otherwise we create a new instance of Channel class and return it.
*
* @private
*
* @param {string} channelType The channel type
* @param {string} [channelID] The channel ID
* @param {object} [custom] Custom data to attach to the channel
*
* @return {channel} The channel object, initialize it using channel.watch()
*/
getChannelById = (
channelType: string,
channelID: string,
custom: ChannelData<ChannelType>,
) => {
if (typeof channelID === 'string' && ~channelID.indexOf(':')) {
throw Error(`Invalid channel id ${channelID}, can't contain the : character`);
}

// only allow 1 channel object per cid
const cid = `${channelType}:${channelIDOrCustom}`;
const cid = `${channelType}:${channelID}`;
if (cid in this.activeChannels) {
const channel = this.activeChannels[cid];
if (Object.keys(custom).length > 0) {
Expand All @@ -1550,11 +1628,11 @@ export class StreamChat<
MessageType,
ReactionType,
UserType
>(this, channelType, channelIDOrCustom, custom);
>(this, channelType, channelID, custom);
this.activeChannels[channel.cid] = channel;

return channel;
}
};

/**
* @deprecated Please use upsertUser() function instead.
Expand Down
171 changes: 170 additions & 1 deletion test/unit/channel.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import chai from 'chai';
import { Channel } from '../../src/channel';
import { generateMsg } from './test-utils';
import { StreamChat } from '../../src/client';
import { v4 as uuidv4 } from 'uuid';
import { generateMsg } from './test-utils/generateMessage';
import { generateMember } from './test-utils/generateMember';
import { generateUser } from './test-utils/generateUser';
import { getOrCreateChannelApi } from './test-utils/getOrCreateChannelApi';
import { generateChannel } from './test-utils/generateChannel';

const expect = chai.expect;

Expand Down Expand Up @@ -159,3 +165,166 @@ describe('Channel _handleChannelEvent', function () {
expect(channel.state.unreadCount).to.be.equal(30);
});
});

describe('Ensure single channel per cid on client activeChannels state', () => {
const clientVish = new StreamChat('', '');
const user = { id: 'user' };
const channelType = 'messaging';

clientVish.connectUser = () => {
clientVish.user = user;
clientVish.userID = user.id;
clientVish.wsPromise = Promise.resolve();
};

clientVish.connectUser();

it('channel created using id - case 1', () => {
clientVish.activeChannels = {};

const channelVishId = uuidv4();
const mockedChannelResponse = generateChannel({
channel: {
id: channelVishId,
},
});

// to mock the channel.watch call
clientVish.post = () =>
getOrCreateChannelApi(mockedChannelResponse).response.data;
const channelVish_copy1 = clientVish.channel('messaging', channelVishId);

const cid = `${channelType}:${channelVishId}`;

expect(Object.keys(clientVish.activeChannels)).to.contain(cid);
expect(clientVish.activeChannels[cid]).to.contain(channelVish_copy1);

channelVish_copy1.watch();
const channelVish_copy2 = clientVish.channel('messaging', channelVishId);
channelVish_copy2.watch();
expect(channelVish_copy1).to.be.equal(channelVish_copy2);
});
it('channel created using id - case 2', () => {
clientVish.activeChannels = {};

const channelVishId = uuidv4();
const mockedChannelResponse = generateChannel({
channel: {
id: channelVishId,
},
});

// to mock the channel.watch call
clientVish.post = () =>
getOrCreateChannelApi(mockedChannelResponse).response.data;

const channelVish_copy1 = clientVish.channel('messaging', channelVishId);

const cid = `${channelType}:${channelVishId}`;

expect(Object.keys(clientVish.activeChannels)).to.contain(cid);
expect(clientVish.activeChannels[cid]).to.contain(channelVish_copy1);

const channelVish_copy2 = clientVish.channel('messaging', channelVishId);

expect(Object.keys(clientVish.activeChannels)).to.contain(cid);
expect(clientVish.activeChannels[cid]).to.contain(channelVish_copy1);

channelVish_copy1.watch();
channelVish_copy2.watch();

expect(channelVish_copy1).to.be.equal(channelVish_copy2);
});

it('channel created using member list - case 1', async () => {
clientVish.activeChannels = {};

// Mock channel.watch call.
const userVish = generateUser();
const userAmin = generateUser();
const memberVish = generateMember({ user: userVish });
const memberAmin = generateMember({ user: userAmin });
const mockedChannelResponse = generateChannel({
members: [memberVish, memberAmin],
});
clientVish.post = () =>
getOrCreateChannelApi(mockedChannelResponse).response.data;

// Lets start testing
const channelVish_copy1 = clientVish.channel('messaging', {
members: [userAmin.id, userVish.id],
});

const tmpCid = `${channelType}:!members-${[userVish.id, userAmin.id]
.sort()
.join(',')}`;

// activeChannels should have tmpCid now.
expect(Object.keys(clientVish.activeChannels)).to.contain(tmpCid);
expect(clientVish.activeChannels[tmpCid]).to.contain(channelVish_copy1);

await channelVish_copy1.watch();

// tempCid should be replaced with actual cid at this point.
expect(Object.keys(clientVish.activeChannels)).to.not.contain(tmpCid);
expect(Object.keys(clientVish.activeChannels)).to.contain(channelVish_copy1.cid);
expect(clientVish.activeChannels[channelVish_copy1.cid]).to.contain(
channelVish_copy1,
);

const channelVish_copy2 = clientVish.channel('messaging', {
members: [userVish.id, userAmin.id],
});

// Should not populate tmpCid again.
expect(Object.keys(clientVish.activeChannels)).to.not.contain(tmpCid);

await channelVish_copy2.watch();
expect(channelVish_copy1).to.be.equal(channelVish_copy2);
});

it('channel created using member list - case 2', async () => {
clientVish.activeChannels = {};

const userVish = generateUser();
const userAmin = generateUser();

const memberVish = generateMember({ user: userVish });
const memberAmin = generateMember({ user: userAmin });

// Case 1 =======================>
const mockedChannelResponse = generateChannel({
members: [memberVish, memberAmin],
});

// to mock the channel.watch call
clientVish.post = () =>
getOrCreateChannelApi(mockedChannelResponse).response.data;

// Case 1 =======================>
const channelVish_copy1 = clientVish.channel('messaging', {
members: [userAmin.id, userVish.id],
});

const tmpCid = `${channelType}:!members-${[userVish.id, userAmin.id]
.sort()
.join(',')}`;

// activeChannels should have tmpCid now.
expect(Object.keys(clientVish.activeChannels)).to.contain(tmpCid);
expect(clientVish.activeChannels[tmpCid]).to.contain(channelVish_copy1);

const channelVish_copy2 = clientVish.channel('messaging', {
members: [userVish.id, userAmin.id],
});

// activeChannels still should have tmpCid now.
expect(Object.keys(clientVish.activeChannels)).to.contain(tmpCid);
expect(clientVish.activeChannels[tmpCid]).to.contain(channelVish_copy2);

await channelVish_copy1.watch();
await channelVish_copy2.watch();

expect(channelVish_copy1).to.be.equal(channelVish_copy2);
});
});
2 changes: 1 addition & 1 deletion test/unit/channel_state.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Immutable from 'seamless-immutable';
import chai from 'chai';
import { ChannelState } from '../../src/channel_state';
import { generateMsg } from './test-utils';
import { generateMsg } from './test-utils/generateMessage';

const expect = chai.expect;

Expand Down
Loading