diff --git a/web/components/message-result.css b/web/components/message-result.css new file mode 100644 --- /dev/null +++ b/web/components/message-result.css @@ -0,0 +1,28 @@ +.messageContainer { + overflow-y: scroll; + border: 1px solid var(--pin-message-modal-border-color); + border-radius: 7px; + max-height: 400px; + margin: 16px 32px; +} + +.messageDate { + color: var(--chat-timestamp-color); + font-size: var(--xs-font-12); + padding: 0px 0px 6px 0px; + line-height: var(--line-height-text); + text-align: left; + margin-left: 16px; +} + +.creator { + font-size: small; + color: var(--shades-white-60); + font-size: var(--s-font-14); + padding: 4px 24px; + text-align: left; +} + +.messageContent { + margin-bottom: 1px; +} diff --git a/web/components/message-result.react.js b/web/components/message-result.react.js new file mode 100644 --- /dev/null +++ b/web/components/message-result.react.js @@ -0,0 +1,57 @@ +// @flow + +import * as React from 'react'; + +import { useStringForUser } from 'lib/hooks/ens-cache.js'; +import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; +import { longAbsoluteDate } from 'lib/utils/date-utils.js'; + +import css from './message-result.css'; +import { MessageListContext } from '../chat/message-list-types.js'; +import Message from '../chat/message.react.js'; +import { useTextMessageRulesFunc } from '../markdown/rules.react.js'; + +type MessageResultProps = { + +item: ChatMessageInfoItem, + +threadInfo: ThreadInfo, +}; + +function MessageResult(props: MessageResultProps): React.Node { + const { item, threadInfo } = props; + + const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo); + const messageListContext = React.useMemo(() => { + if (!getTextMessageMarkdownRules) { + return undefined; + } + return { getTextMessageMarkdownRules }; + }, [getTextMessageMarkdownRules]); + + const shouldShowUsername = !item.startsConversation && !item.startsCluster; + const username = useStringForUser( + shouldShowUsername ? item.messageInfo.creator : null, + ); + + return ( +
+
+
{username}
+
+ + + +
+
+ {longAbsoluteDate(item.messageInfo.time)} +
+
+
+ ); +} + +export default MessageResult; diff --git a/web/modals/chat/toggle-pin-modal.css b/web/modals/chat/toggle-pin-modal.css --- a/web/modals/chat/toggle-pin-modal.css +++ b/web/modals/chat/toggle-pin-modal.css @@ -0,0 +1,30 @@ +.confirmationText { + color: var(--pin-message-information-text-color); + padding: 16px 32px 0 32px; + font-size: small; +} + +.buttonContainer { + width: 100%; + display: flex; + flex-direction: column; + align-self: center; + align-items: stretch; + margin-bottom: 16px; +} + +.togglePinButton { + margin: 0 32px 0 32px; +} + +.cancelButton { + color: white; + display: flex; + justify-content: center; + margin-top: 16px; +} + +.cancelButton:hover { + cursor: pointer; + text-decoration: underline; +} diff --git a/web/modals/chat/toggle-pin-modal.react.js b/web/modals/chat/toggle-pin-modal.react.js --- a/web/modals/chat/toggle-pin-modal.react.js +++ b/web/modals/chat/toggle-pin-modal.react.js @@ -1,18 +1,129 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; +import { + toggleMessagePin, + toggleMessagePinActionTypes, +} from 'lib/actions/thread-actions.js'; +import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; +import { + useServerCall, + useDispatchActionPromise, +} from 'lib/utils/action-utils.js'; + +import css from './toggle-pin-modal.css'; +import Button, { buttonThemes } from '../../components/button.react.js'; +import MessageResult from '../../components/message-result.react.js'; +import Modal from '../modal.react.js'; type TogglePinModalProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, }; -// eslint-disable-next-line no-unused-vars function TogglePinModal(props: TogglePinModalProps): React.Node { - return <>; + const { item, threadInfo } = props; + const { messageInfo, isPinned } = item; + const { popModal } = useModalContext(); + + const callToggleMessagePin = useServerCall(toggleMessagePin); + const dispatchActionPromise = useDispatchActionPromise(); + + const modalInfo = React.useMemo(() => { + if (isPinned) { + return { + name: 'Remove Pinned Message', + action: 'unpin', + confirmationText: + 'Are you sure you want to remove this pinned message?', + buttonText: 'Remove Pinned Message', + buttonColor: buttonThemes.danger, + }; + } + + return { + name: 'Pin Message', + action: 'pin', + confirmationText: `You may pin this message to the channel + you are currently viewing. To unpin a message, select the pinned + messages icon in the channel.`, + buttonText: 'Pin Message', + buttonColor: buttonThemes.standard, + }; + }, [isPinned]); + + // We want to remove inline engagement (threadCreatedFromMessage / reactions) + // and the message header (startsConversation). We also want to set isViewer + // to false so that the message is left-aligned and uncolored. + const modifiedItem = React.useMemo(() => { + if (item.messageInfoType !== 'composable') { + return item; + } + + return { + ...item, + threadCreatedFromMessage: undefined, + reactions: {}, + startsConversation: false, + messageInfo: { + ...item.messageInfo, + creator: { + ...item.messageInfo.creator, + isViewer: false, + }, + }, + }; + }, [item]); + + const onClick = React.useCallback(() => { + const createToggleMessagePinPromise = async () => { + invariant(messageInfo.id, 'messageInfo.id should be defined'); + const result = await callToggleMessagePin({ + messageID: messageInfo.id, + action: modalInfo.action, + }); + return { + newMessageInfos: result.newMessageInfos, + threadID: result.threadID, + }; + }; + + dispatchActionPromise( + toggleMessagePinActionTypes, + createToggleMessagePinPromise(), + ); + popModal(); + }, [ + modalInfo, + callToggleMessagePin, + dispatchActionPromise, + messageInfo.id, + popModal, + ]); + + return ( + +
{modalInfo.confirmationText}
+ +
+ +
+ Cancel +
+
+
+ ); } export default TogglePinModal; diff --git a/web/theme.css b/web/theme.css --- a/web/theme.css +++ b/web/theme.css @@ -212,4 +212,6 @@ --topbar-button-fg: var(--shades-white-60); --message-label-color: var(--shades-black-60); --topbar-lines: rgba(255, 255, 255, 0.08); + --pin-message-information-text-color: var(--shades-white-60); + --pin-message-modal-border-color: var(--shades-black-80); } diff --git a/web/utils/tooltip-action-utils.js b/web/utils/tooltip-action-utils.js --- a/web/utils/tooltip-action-utils.js +++ b/web/utils/tooltip-action-utils.js @@ -192,6 +192,8 @@ isComposableMessageType(messageInfo.type) && threadHasPermission(threadInfo, threadPermissions.MANAGE_PINS); + const inputState = React.useContext(InputStateContext); + return React.useMemo(() => { if (!canTogglePin) { return null; @@ -202,7 +204,11 @@ const buttonContent = ; const onClickTogglePin = () => { - pushModal(); + pushModal( + + + , + ); }; return { @@ -210,7 +216,7 @@ onClick: onClickTogglePin, label: isPinned ? 'Unpin' : 'Pin', }; - }, [canTogglePin, isPinned, pushModal, item, threadInfo]); + }, [canTogglePin, inputState, isPinned, pushModal, item, threadInfo]); } function useMessageTooltipActions(