Skip to content

Commit

Permalink
✨ add 'me' and 'count' to presenceChannel return
Browse files Browse the repository at this point in the history
  • Loading branch information
mayteio committed Jul 11, 2020
1 parent cc894f5 commit 80c017b
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 24 deletions.
6 changes: 2 additions & 4 deletions src/__tests__/useChannel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@ describe("useChannel()", () => {
subscribe: jest.fn(),
unsubscribe: mockUnsubscribe,
};
const wrapper = ({ children }) => (
<__PusherContext.Provider value={{ client: client as any }}>
{children}
</__PusherContext.Provider>
const wrapper: React.FC = (props) => (
<__PusherContext.Provider value={{ client: client as any }} {...props} />
);
const { unmount } = await renderHook(() => useChannel("public-channel"), {
wrapper,
Expand Down
48 changes: 47 additions & 1 deletion src/__tests__/usePresenceChannel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,49 @@ import {
import { PusherMock } from "pusher-js-mock";
import { __PusherContext } from "../PusherProvider";
import { act } from "@testing-library/react-hooks";
import { usePresenceChannel } from "../usePresenceChannel";
import {
usePresenceChannel,
SET_STATE,
presenceChannelReducer,
ADD_MEMBER,
REMOVE_MEMBER,
} from "../usePresenceChannel";

describe("presenceChannelReducer", () => {
/** Default state */
const defaultState = {
members: {},
count: 0,
me: undefined,
myID: undefined,
};

test(SET_STATE, () => {
const state = presenceChannelReducer(defaultState, {
type: SET_STATE,
payload: { count: 1 },
});
expect(state.count).toBe(1);
});

test(ADD_MEMBER, () => {
const state = presenceChannelReducer(defaultState, {
type: ADD_MEMBER,
payload: { id: "their-id", info: {} },
});
expect(state.members).toEqual({ "their-id": {} });
expect(state.count).toBe(1);
});

test(REMOVE_MEMBER, () => {
const state = presenceChannelReducer(
{ ...defaultState, members: { "their-id": {} }, count: 1 },
{ type: REMOVE_MEMBER, payload: { id: "their-id" } }
);
expect(state.members).toEqual({});
expect(state.count).toBe(0);
});
});

describe("usePresenceChannel()", () => {
test('should throw an error if channelName doesn\'t have "presence-" in it', async () => {
Expand Down Expand Up @@ -54,6 +96,8 @@ describe("usePresenceChannel()", () => {

expect(result.current.members).toEqual({ "my-id": {} });
expect(result.current.myID).toEqual("my-id");
expect(result.current.me).toEqual({ id: "my-id", info: {} });
expect(result.current.count).toBe(1);

let otherClient: PusherMock;
await act(async () => {
Expand All @@ -63,10 +107,12 @@ describe("usePresenceChannel()", () => {
});

expect(result.current.members).toEqual({ "my-id": {}, "your-id": {} });
expect(result.current.count).toBe(2);

await act(async () => {
otherClient.unsubscribe("presence-channel");
});
expect(result.current.members).toEqual({ "my-id": {} });
expect(result.current.count).toBe(1);
});
});
110 changes: 91 additions & 19 deletions src/usePresenceChannel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Members, PresenceChannel } from "pusher-js";
import { useEffect, useState } from "react";
import { useEffect, useReducer } from "react";

import invariant from "invariant";
import { useChannel } from "./useChannel";
Expand All @@ -15,41 +15,114 @@ import { useChannel } from "./useChannel";
* const { channel, members, myID } = usePresenceChannel("presence-my-channel");
* ```
*/
export function usePresenceChannel(channelName: string) {

/** Internal state */
type PresenceChannelState = {
members: Record<string, any>;
me: Record<string, any> | undefined;
myID: string | undefined;
count: number;
};

type MemberAction = { id: string; info?: Record<string, any> };

type ReducerAction = {
type: typeof SET_STATE | typeof ADD_MEMBER | typeof REMOVE_MEMBER;
payload: Partial<PresenceChannelState> | MemberAction;
};

/** Hook return value */
interface usePresenceChannelValue extends Partial<PresenceChannelState> {
channel?: PresenceChannel;
}

/** Presence channel reducer to keep track of state */
export const SET_STATE = "set-state";
export const ADD_MEMBER = "add-member";
export const REMOVE_MEMBER = "remove-member";
export const presenceChannelReducer = (
state: PresenceChannelState,
{ type, payload }: ReducerAction
) => {
switch (type) {
/** Generic setState */
case SET_STATE:
return { ...state, ...payload };

/** Member added */
case ADD_MEMBER:
const { id: addedMemberId, info } = payload as MemberAction;
return {
...state,
count: state.count + 1,
members: {
...state.members,
[addedMemberId]: info,
},
};

/** Member removed */
case REMOVE_MEMBER:
const { id: removedMemberId } = payload as MemberAction;
const members = { ...state.members };
delete members[removedMemberId];
return {
...state,
count: state.count - 1,
members: {
...members,
},
};
}
};

export function usePresenceChannel(
channelName: string
): usePresenceChannelValue {
// errors for missing arguments
invariant(
channelName.includes("presence-"),
"Presence channels should use prefix 'presence-' in their name. Use the useChannel hook instead."
);

// Get regular channel functionality
const [members, setMembers] = useState({});
const [myID, setMyID] = useState();
/** Store internal channel state */
const [state, dispatch] = useReducer(presenceChannelReducer, {
members: {},
me: undefined,
myID: undefined,
count: 0,
});

// bind and unbind member events events on our channel
const channel = useChannel<PresenceChannel>(channelName);
useEffect(() => {
if (channel) {
// Get membership info on successful subscription
const handleSubscriptionSuccess = (members: Members) => {
setMembers(members.members);
setMyID(members.myID);
dispatch({
type: SET_STATE,
payload: {
members: members.members,
myID: members.myID,
me: members.me,
count: Object.keys(members.members).length,
},
});
};

// add a member to the members object
// Add member to the members object
const handleAdd = (member: any) => {
setMembers((previousMembers) => ({
...previousMembers,
[member.id]: member.info
}));
dispatch({
type: ADD_MEMBER,
payload: member,
});
};

// remove a member from the members object
// Remove member from the members object
const handleRemove = (member: any) => {
setMembers((previousMembers) => {
const nextMembers: any = { ...previousMembers };
delete nextMembers[member.id];
return nextMembers;
dispatch({
type: REMOVE_MEMBER,
payload: member,
});
};

Expand All @@ -75,7 +148,6 @@ export function usePresenceChannel(channelName: string) {

return {
channel,
members,
myID
...state,
};
}

0 comments on commit 80c017b

Please sign in to comment.