Skip to content
This repository was archived by the owner on May 7, 2023. It is now read-only.

Add self chat feature#474

Merged
hyochan merged 44 commits into
hyochan:masterfrom
rarira:feature_self_chat_new
Sep 1, 2021
Merged

Add self chat feature#474
hyochan merged 44 commits into
hyochan:masterfrom
rarira:feature_self_chat_new

Conversation

@rarira
Copy link
Copy Markdown
Contributor

@rarira rarira commented Aug 26, 2021

Specify project

Both Server and Client

Description

  1. Show UserListItem of mySelf in the top of the Friend Tab, just like KakaoTalk does.
    • My thumbnail is intended to be larger than others
  2. Open ProfileModal of myself if UserListItem mentioned above is pressed.
  3. ProfileModal of myself only renders ProfileUpdate button and Chat with myself button:
    • Block, Report button removed
    • Delete Friend button removed
  4. Chat with myself button finds already opened self chatroom and navigate to that self chat room.
    If failed, it creates ChannelType.self chatroom and navigate to newly created room.
  5. You can also create the self chat room by selecting only yourself after pressing FAB Button in the ChannelCreate
  6. Show small circle view including text(getString('Me')) in front of users name to distinguish a self chat room from others

Simulator Screen Recording - iPhone 12 Pro - 2021-08-26 at 18 47 28

Additional Comments:

  1. below PRs are preferred to be merged before this and i'll make new commit to prevent possible conflicts:
    Add result message when updating profile #470
    Add result message when updating profile #470
  2. lots of updated relay generated files included

Related Issues

  1. Create empty channel (Self Chat?) #459

    • You can't create channel without selecting more than one user
    • If you select only yourself, it means you want self chat room
    • If you select multiple users, yourself is ignored always: ['yourID', 'otherId'] => ['otherId']
  2. Self chat feature #311

    • This is the main issue I've solved

Tests

I added the following tests:
Add almost 100% of test codes for the lines newly added

Checklist

Before you create this PR confirms that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the [CLA].
  • Run yarn lint && yarn tsc
  • Run yarn test or yarn test -u if you need to update snapshot.
  • I am willing to follow-up on review comments in a timely manner.

[wehack2021] - [cloud]

@codecov
Copy link
Copy Markdown

codecov Bot commented Aug 26, 2021

Codecov Report

Merging #474 (28081a8) into master (758429c) will increase coverage by 1.60%.
The diff coverage is 71.91%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #474      +/-   ##
==========================================
+ Coverage   63.05%   64.66%   +1.60%     
==========================================
  Files         110      110              
  Lines        2550     2595      +45     
  Branches      420      429       +9     
==========================================
+ Hits         1608     1678      +70     
+ Misses        810      790      -20     
+ Partials      132      127       -5     
Impacted Files Coverage Δ
...omponents/navigations/MainStackNavigator/index.tsx 3.44% <ø> (ø)
client/src/components/pages/MainChannel.tsx 74.28% <ø> (+2.85%) ⬆️
server/src/resolvers/Friend/query.ts 35.71% <12.50%> (-19.85%) ⬇️
...ts/navigations/MainStackNavigator/ProfileModal.tsx 58.41% <44.44%> (+2.62%) ⬆️
server/src/services/ChannelService.ts 90.90% <57.14%> (-9.10%) ⬇️
client/src/components/pages/ChannelCreate.tsx 56.33% <60.00%> (+19.02%) ⬆️
server/src/resolvers/Channel/mutation.ts 86.20% <62.50%> (-0.55%) ⬇️
client/src/components/pages/MainFriend.tsx 87.50% <100.00%> (+4.16%) ⬆️
client/src/components/uis/ChannelListItem.tsx 100.00% <100.00%> (+34.54%) ⬆️
client/src/components/uis/UserListItem.tsx 87.87% <100.00%> (+9.30%) ⬆️
... and 7 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 758429c...28081a8. Read the comment docs.

@hyochan hyochan added the 🎯 feature New feature label Aug 27, 2021
Copy link
Copy Markdown
Contributor

@JongtaekChoi JongtaekChoi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems really good!

Comment thread client/src/components/pages/__tests__/MainChannel.test.tsx Outdated
Copy link
Copy Markdown
Collaborator

@0916dhkim 0916dhkim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rarira Fabulous work! Also, the test cases look awesome 👍
I approve this PR with a lot of suggestions below 😄

Comment on lines +168 to +169
if (channelType !== ChannelType.self)
await createMemberships(channel.id, filteredPeerUserIds);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to resolve this in this PR, but I think we need some refactoring in this file. Functions are getting quite big.

Comment on lines +89 to +97
export const getChannelType = (
userId: string,
peerUserIds: string[],
): ChannelType => {
if (peerUserIds.length === 1 && peerUserIds[0] === userId)
return ChannelType.self;

return ChannelType.private;
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how this logic is extracted into a separate function; however, I think the name can be more clear. How about we name this isSelfChannel(userId, memberIds): boolean?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intended this function to return ChannelType not boolean because there might be new feature added to server 'public' type channel. But now I know that in that case, we should create totally new different mutation function which is not findOrCreatePrivateChannel..

So I'll make change as you suggested, thanks always

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had changed codes as you suggested but it brings 3 test case failures in server side Channel.test.ts. I think it is because old createNewChannel function was used even when creating public channel..

So I had to revert that commit.

I think we should refactor all related codes when we introduce public channel feature.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Thanks for trying 👍

Comment thread server/src/resolvers/Friend/query.ts Outdated
Comment thread server/src/resolvers/Friend/query.ts
Comment thread client/src/components/pages/ChannelCreate.tsx
Comment thread client/src/components/pages/__tests__/ChannelCreate.test.tsx Outdated
Comment thread client/src/components/pages/__tests__/ChannelCreate.test.tsx Outdated
Comment thread client/src/components/pages/MainFriend.tsx Outdated
Comment thread client/src/components/pages/MainFriend.tsx
@rarira
Copy link
Copy Markdown
Contributor Author

rarira commented Aug 29, 2021

Changed all excludeMe: true to excludeMe:false to update store properly. it's finished, I hope.

@0916dhkim
Copy link
Copy Markdown
Collaborator

0916dhkim commented Aug 30, 2021

@rarira Just making sure you did not miss it. There are 19 hidden comments in my first review. Github automatically folds when there are too many comments in one review 😅
image

@rarira
Copy link
Copy Markdown
Contributor Author

rarira commented Aug 30, 2021

@rarira When useAuthContext hook is mocked, AuthContextProvider is not necessary here (thus you don't need to export it either). You can check how useNavigation hook is mocked here:
https://github.com/dooboolab/hackatalk/blob/18eef96705434ad25b9ae72aeef463f83f9ded2b/client/src/components/pages/__tests__/ChangePw.test.tsx

I'll try once again later. Thanks.

@rarira rarira force-pushed the feature_self_chat_new branch from 7055403 to c94fcf2 Compare August 31, 2021 03:58
@rarira
Copy link
Copy Markdown
Contributor Author

rarira commented Aug 31, 2021

@rarira When useAuthContext hook is mocked, AuthContextProvider is not necessary here (thus you don't need to export it either). You can check how useNavigation hook is mocked here:
https://github.com/dooboolab/hackatalk/blob/18eef96705434ad25b9ae72aeef463f83f9ded2b/client/src/components/pages/__tests__/ChangePw.test.tsx

@0916dhkim
I find that createTestElement function in the testUtils.tsx already mocks context and provide user through wrapping testElement with <AuthProvider initialAuthUser={mockContext?.user}> .
So I think mocking useAuthContext here in test code is unnecessary. I just provide test user as mockContext.user as below and the test passed without a issue.

 it('renders [UserListItem]', async () => {
    const user: AuthProviderMeQueryResponse['me'] = {
      id: 'user-1',
      verified: true,
      email: 'test@email.com',
      profile: null,
    };

    const component = createTestElement(<ChannelCreate />, {
      environment: mockEnvironment,
     /// here I provide test user
      user,
    });
    const screen = render(component);

    const nickname = await screen.findByText('jdoe1234');

    const userListItemWrapper = await screen.getByTestId(
      'userListItem-wrapper',
    );

    expect(nickname).toBeTruthy();
    expect(userListItemWrapper.props.isMyself).toBeTruthy();
  });

@hyochan hyochan requested a review from 0916dhkim August 31, 2021 04:16
@hyochan hyochan linked an issue Aug 31, 2021 that may be closed by this pull request
Copy link
Copy Markdown
Collaborator

@0916dhkim 0916dhkim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rarira Thanks for the followup! I think we are almost there. Just one more iteration and we'll be good to merge 👍

};

const ProfileModalContext = createContext<ProfileModalContext>({
export const ProfileModalContext = createContext<ProfileModalContext>({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think mocking useAuthContext here in test code is unnecessary.

@rarira Thank you for addressing AuthProvider related comments 👍
The point was avoid exporting AuthContextProvider. Can you take similar approach for ProfileModalContext and remove export here?

const screen = render(component);
const btn = await screen.findByTestId('list-item-0');
const screen = render(component);
const channelItemBtn = await screen.findByTestId('list-item-0');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding accessibility label on any icon-only button is a good idea. It helps people who use screen reader.

Comment on lines +153 to +197
class FilteredMemberships {
constructor(
private memberships: Maybe<Membership[]> | undefined,
private user: AuthProviderMeQueryResponse['me'] | null,
private channelType: string,
) {
this.memberships = memberships;
this.channelType = channelType;
this.user = user;
}

private get filtered(): (Maybe<Membership> | undefined)[] | undefined {
return this.memberships?.filter((member) => {
if (this.channelType !== 'self')
return member?.user?.id !== this.user?.id;

return true;
});
}

get users(): (Maybe<User> | undefined)[] | undefined {
return this.filtered?.map((membership) => membership?.user);
}

get userNames(): string[] | undefined {
return this.users?.map((v) => v?.nickname || v?.name || '');
}
calculatedPhotoUrls(): (Maybe<string> | undefined)[] | undefined {
return this.filtered?.map(
(membership) => membership?.user?.thumbURL || membership?.user?.photoURL,
);
}

calculateOnlineStatus(): Maybe<Boolean> | undefined {
return this.filtered?.[0]?.user?.isOnline;
}

calculateDisplayName(): Maybe<string> | undefined {
return this.users?.length === 1
? this.users?.[0]?.nickname ||
this.users?.[0]?.name ||
getString('NO_NAME')
: this.userNames?.join(', ');
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think functions are simpler than a class. Something like this:

const calculateUsers = (memberships: Membership[], channelType: string, authUserId): (Maybe<User>)[] => 
  memberships.filter((member) => channelType !== 'self' && member?.user?.id !== authUserId);

const calculatePhotoUrls = (memberships: Membership[]): (Maybe<string>)[] =>
  memberships.map((member) => member?.user?.thumbURL || member?.user?.photoURL);

const calculateOnlineStatus = (users: (Maybe<User>)[]): boolean =>
  users[0]?.isOnline ?? false;

Copy link
Copy Markdown
Contributor Author

@rarira rarira Aug 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, calculatePhotoUrls also needs 'filtered by channel type' memberships passed as an argument, So there needs to be filteredMemberships calculated first and the calculated filteredMemberships is only used for this functions/class method.

if someone pass normal memberships array to calculatePhotoUrls function which is okay in tsc check but we can have unwanted results So I think the reusability of that function is very low.

});

it('renders [StyledMeCircleView] when channelType is "self"', () => {
TEST_CHANNEL.channelType = 'self';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you forgot to copy this TEST_CHANNEL

Comment on lines +47 to +52
if (includeMe) {
const myProfile = await prisma.user.findUnique({
where: {
id: userId,
},
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it had some problem with sorting, I mean, I wanted to display 'ME' always at the top

@rarira It makes sense. Let's leave it like this for now.

@rarira
Copy link
Copy Markdown
Contributor Author

rarira commented Aug 31, 2021

@0916dhkim

#474 (comment)

I don't know why I can't reply this review comment..

I'm afraid I can't take similar approach to ProfileModalProvider as AuthProvider
Because createTestElement in testUtils return

 <DeviceProvider>
      <ThemeProvider
        initialThemeType={mockContext?.themeType ?? 'dark'}
        customTheme={{light, dark}}>
        <ResettableRelayProvider
          environment={mockContext?.environment ?? createMockEnvironment()}
          createRelayEnvironment={createMockEnvironment}>
          <Suspense fallback={<Text>TEST FALLBACK</Text>}>
            <AuthProvider initialAuthUser={mockContext?.user}>
              <ProfileModalProvider>
                <TestSafeAreaProvider>{child}</TestSafeAreaProvider>
              </ProfileModalProvider>
            </AuthProvider>
          </Suspense>
        </ResettableRelayProvider>
      </ThemeProvider>
    </DeviceProvider>

AuthProvider has prop which can be used to pass mockContext?.user but ProfileModalProvider doesn't have such a prop. Of course I can add new prop like initialModalState to ProfileModalProvider. But I don't know which one is better to add export or new prop only for testing.

Copy link
Copy Markdown
Collaborator

@0916dhkim 0916dhkim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rarira Thank you for quick followup!

But I don't know which one is better to add export or new prop only for testing.

I suggest adding new prop for testing. Both approaches break encapsulation for testing as you mentioned, but it is less likely to make mistake with the prop than with exports. If you are concerned, we can name the prop something like testModalState (as opposed to initialModalState) so other developers do not get confused about the purpose of that prop.

Comment on lines +167 to +176
const calculatePhotoUrls = (
memberships: (Maybe<Membership> | undefined)[] | undefined,
): (Maybe<string> | undefined)[] | undefined =>
memberships?.map(
(membership) => membership?.user?.thumbURL || membership?.user?.photoURL,
);

const calculateOnlineStatus = (
memberships: (Maybe<Membership> | undefined)[] | undefined,
): Maybe<Boolean> | undefined => memberships?.[0]?.user?.isOnline;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calculatePhotoUrls & calculateOnlineStatus can take users parameter like calculateDisplayNames function does. You can then combine calculateFilteredMemberships & calculateUsers functions into one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I finally find jest.spyOn example Message.test.tsx and update MainFriend.test.tsx
and update those calculate functions as suggested.. now it looks simple :)

Copy link
Copy Markdown
Owner

@hyochan hyochan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rarira Superb work on this~~!! I've just made some comments here that can be enhanced!

{modalState.isMyself ? (
<TouchableOpacity
testID="profile-update-button"
style={{padding: 8}}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood

Comment thread client/src/components/navigations/MainStackNavigator/ProfileModal.tsx Outdated
Comment thread client/src/components/navigations/__tests__/ProfileModal.test.tsx Outdated
Comment thread client/src/components/navigations/__tests__/ProfileModal.test.tsx Outdated
Comment thread client/src/components/navigations/__tests__/ProfileModal.test.tsx Outdated
Comment thread client/src/components/pages/ChannelCreate.tsx
Comment thread client/src/components/pages/MainChannel.tsx Outdated
Comment thread client/src/components/uis/ChannelListItem.tsx
Comment thread client/src/components/uis/ChannelListItem.tsx Outdated
Copy link
Copy Markdown
Owner

@hyochan hyochan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rarira You are a rock star ⭐

@hyochan hyochan merged commit 5bdebc2 into hyochan:master Sep 1, 2021
@hyochan hyochan mentioned this pull request Sep 1, 2021
5 tasks
hyochan added a commit that referenced this pull request Sep 1, 2021
* Fix rendering issue in [UserListItem]

* Hotfix - statusMessage rendering throw error on empty string

* Remove the borderColor in profile `viewBtn`
@rarira rarira deleted the feature_self_chat_new branch September 1, 2021 13:03
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

🎯 feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create empty channel (Self Chat?)

5 participants