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

feat: add unstar, unpin, copy link, and navigate options in starred and pinned messages list #744

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
14 changes: 13 additions & 1 deletion packages/react/src/hooks/useFetchChatData.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ const useFetchChatData = (showRoles) => {
const setViewUserInfoPermissions = useUserStore(
(state) => state.setViewUserInfoPermissions
);
const setUserPinPermissions = useUserStore(
(state) => state.setUserPinPermissions
);
const setEditMessagePermissions = useMessageStore(
(state) => state.setEditMessagePermissions
);

const getMessagesAndRoles = useCallback(
async (anonymousMode) => {
Expand Down Expand Up @@ -73,7 +79,13 @@ const useFetchChatData = (showRoles) => {
}

const permissions = await RCInstance.permissionInfo();
setViewUserInfoPermissions(permissions.update[70]);
const permissionsMap = permissions.update.reduce((map, item) => {
map[item._id] = item;
return map;
});
setUserPinPermissions(permissionsMap['pin-message']);
setEditMessagePermissions(permissionsMap['edit-message']);
setViewUserInfoPermissions(permissionsMap['view-full-other-user-info']);
} catch (e) {
console.error(e);
}
Expand Down
16 changes: 1 addition & 15 deletions packages/react/src/hooks/useRCAuth.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { useContext } from 'react';
import { useToastBarDispatch } from '@embeddedchat/ui-elements';
import RCContext from '../context/RCInstance';
import {
useUserStore,
totpModalStore,
useLoginStore,
useMessageStore,
} from '../store';
import { useUserStore, totpModalStore, useLoginStore } from '../store';

export const useRCAuth = () => {
const { RCInstance } = useContext(RCContext);
Expand All @@ -25,18 +20,11 @@ export const useRCAuth = () => {
);
const setPassword = useUserStore((state) => state.setPassword);
const setEmailorUser = useUserStore((state) => state.setEmailorUser);
const setUserPinPermissions = useUserStore(
(state) => state.setUserPinPermissions
);
const setEditMessagePermissions = useMessageStore(
(state) => state.setEditMessagePermissions
);
const dispatchToastMessage = useToastBarDispatch();

const handleLogin = async (userOrEmail, password, code) => {
try {
const res = await RCInstance.login(userOrEmail, password, code);
const permissions = await RCInstance.permissionInfo();
if (res.error === 'Unauthorized' || res.error === 403) {
dispatchToastMessage({
type: 'error',
Expand Down Expand Up @@ -68,8 +56,6 @@ export const useRCAuth = () => {
setIsTotpModalOpen(false);
setEmailorUser(null);
setPassword(null);
setUserPinPermissions(permissions.update[150]);
setEditMessagePermissions(permissions.update[28]);
dispatchToastMessage({
type: 'success',
message: 'Successfully logged in',
Expand Down
11 changes: 0 additions & 11 deletions packages/react/src/views/EmbeddedChat.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ const EmbeddedChat = (props) => {
const [isSynced, setIsSynced] = useState(!remoteOpt);
const { getToken, saveToken, deleteToken } = getTokenStorage(secure);
const {
isUserAuthenticated,
setIsUserAuthenticated,
setUsername: setAuthenticatedUsername,
setUserAvatarUrl: setAuthenticatedAvatarUrl,
Expand All @@ -84,13 +83,6 @@ const EmbeddedChat = (props) => {
}));

const setIsLoginIn = useLoginStore((state) => state.setIsLoginIn);
const setUserPinPermissions = useUserStore(
(state) => state.setUserPinPermissions
);

const setEditMessagePermissions = useMessageStore(
(state) => state.setEditMessagePermissions
);
if (isClosable && !setClosableState) {
throw Error(
'Please provide a setClosableState to props when isClosable = true'
Expand Down Expand Up @@ -132,9 +124,6 @@ const EmbeddedChat = (props) => {
setIsLoginIn(true);
try {
await RCInstance.autoLogin(auth);
const permissions = await RCInstance.permissionInfo();
setUserPinPermissions(permissions.update[150]);
setEditMessagePermissions(permissions.update[28]);
} catch (error) {
console.error(error);
} finally {
Expand Down
28 changes: 26 additions & 2 deletions packages/react/src/views/MessageAggregators/PinnedMessages.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
import React from 'react';
import { useComponentOverrides } from '@embeddedchat/ui-elements';
import React, { useContext } from 'react';
import {
useComponentOverrides,
useToastBarDispatch,
} from '@embeddedchat/ui-elements';
import { MessageAggregator } from './common/MessageAggregator';
import RCContext from '../../context/RCInstance';

const PinnedMessages = () => {
const { variantOverrides } = useComponentOverrides('PinnedMessages');
const viewType = variantOverrides.viewType || 'Sidebar';
const { RCInstance } = useContext(RCContext);
const dispatchToastMessage = useToastBarDispatch();

const unpin = async (msg) => {
const isPinned = msg.pinned;
const Unpin = await RCInstance.unpinMessage(msg._id);
if (Unpin.error) {
dispatchToastMessage({
type: 'error',
message: 'Error pinning message',
});
} else {
dispatchToastMessage({
type: 'success',
message: isPinned ? 'Message unpinned' : 'Message pinned',
});
}
};
return (
<MessageAggregator
title="Pinned Messages"
iconName="pin"
unpin={unpin}
isPinnedMessageDisplay
noMessageInfo="No Pinned Messages"
shouldRender={(msg) => msg.pinned}
viewType={viewType}
Expand Down
25 changes: 23 additions & 2 deletions packages/react/src/views/MessageAggregators/StarredMessages.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React, { useCallback, useEffect } from 'react';
import { useComponentOverrides } from '@embeddedchat/ui-elements';
import React, { useCallback, useContext } from 'react';
import {
useComponentOverrides,
useToastBarDispatch,
} from '@embeddedchat/ui-elements';
import { useStarredMessageStore, useUserStore } from '../../store';
import { MessageAggregator } from './common/MessageAggregator';
import RCContext from '../../context/RCInstance';

const StarredMessages = () => {
const authenticatedUserId = useUserStore((state) => state.userId);
Expand All @@ -10,16 +14,33 @@ const StarredMessages = () => {
const starredMessages = useStarredMessageStore(
(state) => state.starredMessages
);
const setStarredMessages = useStarredMessageStore(
(state) => state.setStarredMessages
);
const dispatchToastMessage = useToastBarDispatch();
const { RCInstance } = useContext(RCContext);
const shouldRender = useCallback(
(msg) =>
msg.starred &&
msg.starred.some((star) => star._id === authenticatedUserId),
[authenticatedUserId]
);

const unstar = async (msg) => {
await RCInstance.unstarMessage(msg._id);
dispatchToastMessage({
type: 'success',
message: 'Message unstarred',
});
setStarredMessages(starredMessages.filter((str) => str._id !== msg._id));
};

return (
<MessageAggregator
title="Starred Messages"
iconName="star"
isStarredMessageDisplay
unstar={unstar}
noMessageInfo="No Starred Messages"
fetchedMessageList={starredMessages}
shouldRender={shouldRender}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useContext } from 'react';
import { isSameDay, format, set } from 'date-fns';
import {
Box,
Menu,
Sidebar,
Popup,
useTheme,
ActionButton,
Icon,
useToastBarDispatch,
} from '@embeddedchat/ui-elements';
import RCContext from '../../../context/RCInstance';
import { MessageDivider } from '../../Message/MessageDivider';
import Message from '../../Message/Message';
import getMessageAggregatorStyles from './MessageAggregator.styles';
import { useMessageStore, useSidebarStore } from '../../../store';
import { useMessageStore, useSidebarStore, useUserStore } from '../../../store';
import { useSetMessageList } from '../../../hooks/useSetMessageList';
import LoadingIndicator from './LoadingIndicator';
import NoMessagesIndicator from './NoMessageIndicator';
import FileDisplay from '../../FileMessage/FileMessage';
import useSetExclusiveState from '../../../hooks/useSetExclusiveState';
import { useRCContext } from '../../../context/RCInstance';

export const MessageAggregator = ({
title,
iconName,
isStarredMessageDisplay = false,
isPinnedMessageDisplay = false,
unstar,
unpin,
noMessageInfo,
shouldRender,
fetchedMessageList,
Expand All @@ -34,9 +40,10 @@ export const MessageAggregator = ({
const { theme } = useTheme();
const styles = getMessageAggregatorStyles(theme);
const setExclusiveState = useSetExclusiveState();
const { ECOptions } = useRCContext();
const { RCInstance, ECOptions } = useContext(RCContext);
const showRoles = ECOptions?.showRoles;
const messages = useMessageStore((state) => state.messages);
const currentUserRoles = useUserStore((state) => state.roles);
const threadMessages = useMessageStore((state) => state.threadMessages) || [];
const allMessages = useMemo(
() => [...messages, ...threadMessages],
Expand All @@ -47,6 +54,13 @@ export const MessageAggregator = ({
fetchedMessageList || searchFiltered || allMessages,
shouldRender
);
const dispatchToastMessage = useToastBarDispatch();
const pinPermissions = useUserStore(
(state) => state.userPinPermissions.roles
);

const pinRoles = new Set(pinPermissions);
const isAllowedToPin = currentUserRoles.some((role) => pinRoles.has(role));

const setShowSidebar = useSidebarStore((state) => state.setShowSidebar);
const openThread = useMessageStore((state) => state.openThread);
Expand Down Expand Up @@ -99,6 +113,28 @@ export const MessageAggregator = ({
}
};

const getMessageLink = async (id) => {
const host = await RCInstance.getHost();
const res = await RCInstance.channelInfo();
return `${host}/channel/${res.room.name}/?msg=${id}`;
};

const copyLink = async (id) => {
try {
const messageLink = await getMessageLink(id);
await navigator.clipboard.writeText(messageLink);
dispatchToastMessage({
type: 'success',
message: 'Message link copied successfully',
});
} catch (err) {
dispatchToastMessage({
type: 'error',
message: 'Error in copying message link',
});
}
};

const isMessageNewDay = (current, previous) =>
!previous ||
shouldRender(previous) ||
Expand Down Expand Up @@ -179,18 +215,60 @@ export const MessageAggregator = ({
}}
/>

<ActionButton
square
ghost
onClick={() => setJumpToMessage(msg)}
css={{
position: 'relative',
zIndex: 10,
marginRight: '5px',
}}
>
<Icon name="arrow-back" size="1.25rem" />
</ActionButton>
{!isStarredMessageDisplay && !isPinnedMessageDisplay && (
<ActionButton
square
ghost
onClick={() => setJumpToMessage(msg)}
css={{
position: 'relative',
zIndex: 10,
marginRight: '5px',
}}
>
<Icon name="arrow-back" size="1.25rem" />
</ActionButton>
)}
{(isStarredMessageDisplay || isPinnedMessageDisplay) && (
<Box
style={{
marginRight: '20px',
}}
>
<Menu
isToolTip={false}
options={[
isPinnedMessageDisplay && isAllowedToPin
? {
id: 'unpin',
action: () => unpin(msg),
label: 'Unpin',
icon: 'pin',
}
: isStarredMessageDisplay
? {
id: 'unstar',
action: () => unstar(msg),
label: 'Remove star',
icon: 'star',
}
: {},
{
id: 'copyLink',
action: () => copyLink(msg._id),
label: 'Copy link',
icon: 'link',
},
{
id: 'jumptomessage',
action: () => setJumpToMessage(msg),
label: 'Jump to message',
icon: 'arrow-jump',
},
]}
/>
</Box>
)}
</Box>
)}
</React.Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const getMessageAggregatorStyles = () => {
justify-content: initial;
align-items: initial;
max-width: 100%;
overflow-y: scroll;
padding-right: 0;
`,

noMessageStyles: css`
Expand Down
9 changes: 9 additions & 0 deletions packages/ui-elements/src/components/Icon/icons/ArrowJump.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

const ArrowJump = (props) => (
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M8.00003 6.5C8.00003 5.94772 8.44774 5.5 9.00003 5.5H17.5C18.0523 5.5 18.5 5.94772 18.5 6.5V22.5858L21.2929 19.7929C21.6834 19.4024 22.3166 19.4024 22.7071 19.7929C23.0977 20.1834 23.0977 20.8166 22.7071 21.2071L18.2071 25.7071C17.8166 26.0976 17.1834 26.0976 16.7929 25.7071L12.2929 21.2071C11.9024 20.8166 11.9024 20.1834 12.2929 19.7929C12.6834 19.4024 13.3166 19.4024 13.7071 19.7929L16.5 22.5858V7.5H10V13.5C10 14.0523 9.55231 14.5 9.00003 14.5C8.44774 14.5 8.00003 14.0523 8.00003 13.5V6.5Z" />
</svg>
);

export default ArrowJump;
2 changes: 2 additions & 0 deletions packages/ui-elements/src/components/Icon/icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import Arc from './Arc';
import Avatar from './Avatar';
import FormatText from './FormatText';
import Cog from './Cog';
import ArrowJump from './ArrowJump';

const icons = {
file: File,
Expand Down Expand Up @@ -126,6 +127,7 @@ const icons = {
avatar: Avatar,
'format-text': FormatText,
cog: Cog,
'arrow-jump': ArrowJump,
};

export default icons;
Loading