diff --git a/app/src/main/helpers/cursorSettings.ts b/app/src/main/helpers/cursorSettings.ts index 3cc85909cd..bae4cee1d3 100644 --- a/app/src/main/helpers/cursorSettings.ts +++ b/app/src/main/helpers/cursorSettings.ts @@ -41,6 +41,106 @@ const hideSystemCursorCss = ` *:focus:not(:focus-visible) { cursor: none !important; } + + .react-player-hide-cursor { + cursor: none !important; + } + + video::-webkit-media-controls-panel { + cursor: none !important; + } + + video::-webkit-media-controls-play-button { + cursor: none !important; + } + + video::-webkit-media-controls-volume-slider-container { + cursor: none !important; + } + + video::-webkit-media-controls-volume-slider { + cursor: none !important; + } + + video::-webkit-media-controls-mute-button { + cursor: none !important; + } + + video::-webkit-media-controls-timeline { + cursor: none !important; + } + + video::-webkit-media-controls-current-time-display { + cursor: none !important; + } + + video::-webkit-full-page-media::-webkit-media-controls-panel { + cursor: none !important; + } + + video::-webkit-media-controls-panel { + cursor: none !important; + } + + video::-webkit-media-controls-start-playback-button { + cursor: none !important; + } + + video::-webkit-media-controls-overlay-play-button { + cursor: none !important; + } + + video::-webkit-media-controls-toggle-closed-captions-button { + cursor: none !important; + } + + video::-webkit-media-controls-status-display { + cursor: none !important; + } + + video::-webkit-media-controls-mouse-display { + cursor: none !important; + } + + video::-webkit-media-controls-timeline-container { + cursor: none !important; + } + + video::-webkit-media-controls-time-remaining-display { + cursor: none !important; + } + + video::-webkit-media-controls-seek-back-button { + cursor: none !important; + } + + video { + cursor: none !important; + } + + video::-webkit-media-controls-seek-forward-button { + cursor: none !important; + } + + video::-webkit-media-controls-fullscreen-button { + cursor: none !important; + } + + video::-webkit-media-controls-enclosure { + cursor: none !important; + } + + video::-webkit-media-controls-rewind-button { + cursor: none !important; + } + + video::-webkit-media-controls-return-to-realtime-button { + cursor: none !important; + } + + video::-webkit-media-controls-toggle-closed-captions-button { + cursor: none !important; + } `; const showSystemCursorCss = ` diff --git a/app/src/main/windows.ts b/app/src/main/windows.ts index 4d8c6d75ea..67c9238bcc 100644 --- a/app/src/main/windows.ts +++ b/app/src/main/windows.ts @@ -181,5 +181,11 @@ export const createStandaloneChatWindow = () => { newStandaloneChatWindow.show(); }); + // Open standalone URLs in the user's default browser. + newStandaloneChatWindow.webContents.setWindowOpenHandler((edata) => { + shell.openExternal(edata.url); + return { action: 'deny' }; + }); + return newStandaloneChatWindow; }; diff --git a/app/src/renderer/apps/Courier/components/ChatMessage.tsx b/app/src/renderer/apps/Courier/components/ChatMessage.tsx index 0c5fa078cc..e9fe485f7f 100644 --- a/app/src/renderer/apps/Courier/components/ChatMessage.tsx +++ b/app/src/renderer/apps/Courier/components/ChatMessage.tsx @@ -8,6 +8,7 @@ import { MenuItemProps, OnReactionPayload, } from '@holium/design-system'; +import { useToggle } from '@holium/design-system/util'; import { useContextMenu } from 'renderer/components'; import { useAppState } from 'renderer/stores/app.store'; @@ -39,6 +40,8 @@ export const ChatMessagePresenter = ({ const isOur = message.sender === ourShip; const { getOptions, setOptions, defaultOptions } = useContextMenu(); + const deleting = useToggle(false); + const messageRowId = useMemo(() => `message-row-${message.id}`, [message.id]); const isPinned = selectedChat?.isMessagePinned(message.id); const { color: authorColor, nickname: authorNickname } = useMemo(() => { @@ -233,9 +236,11 @@ export const ChatMessagePresenter = ({ icon: 'Trash', iconColor: '#ff6240', labelColor: '#ff6240', - onClick: (evt: React.MouseEvent) => { + onClick: async (evt: React.MouseEvent) => { evt.stopPropagation(); - selectedChat.deleteMessage(message.id); + deleting.toggleOn(); + await selectedChat.deleteMessage(message.id); + deleting.toggleOff(); }, }); } @@ -312,6 +317,7 @@ export const ChatMessagePresenter = ({ ourShip={ourShip} ourColor={ourColor} isEditing={selectedChat?.isEditing(message.id)} + isDeleting={deleting.isOn} updatedAt={message.updatedAt} isEdited={hasEdited} author={message.sender} diff --git a/app/src/renderer/apps/Courier/views/ChatLog/ChatLog.tsx b/app/src/renderer/apps/Courier/views/ChatLog/ChatLog.tsx index 8f872ffd25..f94e3e2ff7 100644 --- a/app/src/renderer/apps/Courier/views/ChatLog/ChatLog.tsx +++ b/app/src/renderer/apps/Courier/views/ChatLog/ChatLog.tsx @@ -101,7 +101,7 @@ export const ChatLogPresenter = ({ isStandaloneChat = false }: Props) => { const showPin = selectedChat.pinnedMessageId !== null && !selectedChat.hidePinned; - const containerWidth = dimensions.width - 24; + const containerWidth = isStandaloneChat ? 434 : dimensions.width - 24; const onMessageSend = async (fragments: any[]) => { if (!selectedChat) return; diff --git a/app/src/renderer/apps/Courier/views/ChatLog/ChatLogBody.tsx b/app/src/renderer/apps/Courier/views/ChatLog/ChatLogBody.tsx index 7da4878606..707c1e6741 100644 --- a/app/src/renderer/apps/Courier/views/ChatLog/ChatLogBody.tsx +++ b/app/src/renderer/apps/Courier/views/ChatLog/ChatLogBody.tsx @@ -1,7 +1,7 @@ -import { RefObject, useMemo, useState } from 'react'; +import { RefObject } from 'react'; import { observer } from 'mobx-react'; -import { Flex, Text, WindowedListRef } from '@holium/design-system/general'; +import { Flex, WindowedListRef } from '@holium/design-system/general'; import { ChatFragmentMobxType, @@ -11,13 +11,8 @@ import { useShipStore } from 'renderer/stores/ship.store'; import { ChatInputBox } from '../../components/ChatInputBox'; import { ChatLogHeader } from '../../components/ChatLogHeader'; -import { PinnedContainer } from '../../components/PinnedMessage'; -import { - ChatInputContainer, - ChatLogListContainer, - FullWidthAnimatePresence, -} from './ChatLogBody.styles'; -import { ChatLogList } from './ChatLogList'; +import { ChatInputContainer } from './ChatLogBody.styles'; +import { ChatLogBodyList } from './ChatLogBodyList'; type Props = { path: string; @@ -57,90 +52,20 @@ const ChatLogBodyPresenter = ({ const { chatStore } = useShipStore(); const { selectedChat, setSubroute, inboxLoader } = chatStore; - const [showAttachments, setShowAttachments] = useState(false); - const messages = selectedChat?.messages ?? []; let topPadding = 0; - let endPadding = 0; if (showPin) { topPadding = 50; } - if (showAttachments) { - endPadding = 136; - } - if (selectedChat?.replyingMsg) { - endPadding = 56; - } - const onAttachmentChange = (attachmentCount: number) => { - if (attachmentCount > 0) { - setShowAttachments(true); - } else { - setShowAttachments(false); - } + const onAttachmentChange = () => { // Wait for transition to finish, then scroll to bottom. setTimeout(() => { listRef.current?.scrollToIndex(messages.length - 1); }, 250); }; - const lastMessageIndex = useMemo( - () => - messages[messages.length - 1]?.createdAt + - messages[messages.length - 1]?.updatedAt, - [ - messages[messages.length - 1]?.createdAt, - messages[messages.length - 1]?.updatedAt, - ] - ); - - const messageList = useMemo(() => { - if (messages.length === 0 && inboxLoader.isLoaded) { - return ( - - You haven't sent or received any messages in this chat yet. - - ); - } - - return ( - - {showPin && pinnedChatMessage && ( - - { - const index = messages.findIndex( - (msg) => msg.id === pinnedChatMessage.id - ); - listRef?.current?.scrollToIndex({ - index, - align: 'start', - behavior: 'smooth', - }); - }} - /> - - )} - - - ); - }, [ - lastMessageIndex, - inboxLoader.isLoaded, - pinnedChatMessage, - showPin, - ourColor, - ]); - return ( - {messageList} + {selectedChat && ( diff --git a/app/src/renderer/apps/Courier/views/ChatLog/ChatLogBodyList.tsx b/app/src/renderer/apps/Courier/views/ChatLog/ChatLogBodyList.tsx new file mode 100644 index 0000000000..c31d837f1b --- /dev/null +++ b/app/src/renderer/apps/Courier/views/ChatLog/ChatLogBodyList.tsx @@ -0,0 +1,73 @@ +import { RefObject } from 'react'; + +import { Text, WindowedListRef } from '@holium/design-system/general'; + +import { ChatMessageType } from 'renderer/stores/models/chat.model'; + +import { PinnedContainer } from '../../components/PinnedMessage'; +import { + ChatLogListContainer, + FullWidthAnimatePresence, +} from './ChatLogBody.styles'; +import { ChatLogList } from './ChatLogList'; + +type Props = { + messages: ChatMessageType[]; + listRef: RefObject; + topPadding: number; + ourColor: string; + showPin: boolean; + pinnedChatMessage: ChatMessageType | null | undefined; + isStandaloneChat: boolean; + isEmpty: boolean; + isLoaded: boolean; +}; + +export const ChatLogBodyList = ({ + messages, + listRef, + topPadding, + ourColor, + showPin, + isEmpty, + isLoaded, + isStandaloneChat, + pinnedChatMessage, +}: Props) => { + if (isEmpty && isLoaded) { + return ( + + You haven't sent or received any messages in this chat yet. + + ); + } + + return ( + + {showPin && pinnedChatMessage && ( + + { + const index = messages.findIndex( + (msg) => msg.id === pinnedChatMessage.id + ); + listRef?.current?.scrollToIndex({ + index, + align: 'start', + behavior: 'smooth', + }); + }} + /> + + )} + + + ); +}; diff --git a/app/src/renderer/apps/Courier/views/ChatLog/ChatLogList.tsx b/app/src/renderer/apps/Courier/views/ChatLog/ChatLogList.tsx index 3a5059db65..261f7f450f 100644 --- a/app/src/renderer/apps/Courier/views/ChatLog/ChatLogList.tsx +++ b/app/src/renderer/apps/Courier/views/ChatLog/ChatLogList.tsx @@ -18,7 +18,6 @@ type Props = { messages: ChatMessageType[]; ourColor: string; isStandaloneChat: boolean; - endOfListPadding?: number; topOfListPadding?: number; }; @@ -26,7 +25,6 @@ export const ChatLogList = ({ listRef, messages: unfilteredMessages, ourColor, - endOfListPadding, topOfListPadding, isStandaloneChat, }: Props) => { @@ -118,19 +116,10 @@ export const ChatLogList = ({ listRef?.current?.scrollBy({ top: 10, }); - } else if (height - prevHeight === endOfListPadding) { - listRef?.current?.scrollBy({ - top: endOfListPadding, - }); } setPrevHeight(height); }} itemContent={renderChatRow} - components={{ - Footer: () => { - return
; - }, - }} chatMode shiftScrollbar={!isStandaloneChat} /> diff --git a/app/src/renderer/apps/Courier/views/Inbox/Inbox.tsx b/app/src/renderer/apps/Courier/views/Inbox/Inbox.tsx index 823226914c..63ff04227b 100644 --- a/app/src/renderer/apps/Courier/views/Inbox/Inbox.tsx +++ b/app/src/renderer/apps/Courier/views/Inbox/Inbox.tsx @@ -14,7 +14,9 @@ type Props = { export const InboxPresenter = ({ isStandaloneChat = false }: Props) => { const { loggedInAccount, shellStore } = useAppState(); const { chatStore, spacesStore } = useShipStore(); - const { sortedChatList, setChat, setSubroute, isChatPinned } = chatStore; + + const { sortedChatList, loader, setChat, setSubroute, isChatPinned } = + chatStore; const currentSpace = spacesStore.selected; useEffect(() => { @@ -27,6 +29,7 @@ export const InboxPresenter = ({ isStandaloneChat = false }: Props) => { accountIdentity={loggedInAccount?.serverId} spacePath={currentSpace?.path} isStandaloneChat={isStandaloneChat} + isLoading={loader.isLoading} isChatPinned={isChatPinned} onClickInbox={(path) => { setChat(path); diff --git a/app/src/renderer/apps/Courier/views/Inbox/InboxBody.styles.ts b/app/src/renderer/apps/Courier/views/Inbox/InboxBody.styles.ts new file mode 100644 index 0000000000..74771abafa --- /dev/null +++ b/app/src/renderer/apps/Courier/views/Inbox/InboxBody.styles.ts @@ -0,0 +1,53 @@ +import styled from 'styled-components'; + +import { Flex } from '@holium/design-system/general'; + +export const InboxListContainer = styled(Flex)<{ isStandaloneChat: boolean }>` + flex: 1; + + .chat-inbox-row-top-pinned { + .chat-inbox-row { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + } + } + .chat-inbox-row-pinned { + .chat-inbox-row { + border-top-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + } + } + .chat-inbox-row-bottom-pinned { + .chat-inbox-row { + border-top-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + } + } + + ${({ isStandaloneChat }) => + isStandaloneChat && + ` + padding-left: 12px; + `} +`; + +export const InboxBodyHeaderContainer = styled(Flex)<{ + isStandaloneChat?: boolean; +}>` + align-items: center; + z-index: 1; + padding: 0 0 8px 4px; + + ${({ isStandaloneChat }) => + isStandaloneChat && + ` + height: 58px; + padding: 12px 12px 12px 24px; + `} +`; diff --git a/app/src/renderer/apps/Courier/views/Inbox/InboxBody.tsx b/app/src/renderer/apps/Courier/views/Inbox/InboxBody.tsx index e6c04fa22b..90cc0e5484 100644 --- a/app/src/renderer/apps/Courier/views/Inbox/InboxBody.tsx +++ b/app/src/renderer/apps/Courier/views/Inbox/InboxBody.tsx @@ -1,64 +1,19 @@ import { useCallback, useState } from 'react'; -import styled from 'styled-components'; -import { - Button, - Flex, - Icon, - Text, - WindowedList, -} from '@holium/design-system/general'; +import { Button, Flex, Icon } from '@holium/design-system/general'; import { TextInput } from '@holium/design-system/inputs'; import { ChatModelType } from 'renderer/stores/models/chat.model'; -import { InboxRow } from './InboxRow'; - -const InboxListContainer = styled(Flex)` - flex: 1; - .chat-inbox-row-top-pinned { - .chat-inbox-row { - border-top-left-radius: 6px; - border-top-right-radius: 6px; - border-bottom-left-radius: 0px; - border-bottom-right-radius: 0px; - } - } - .chat-inbox-row-pinned { - .chat-inbox-row { - border-top-left-radius: 0px; - border-top-right-radius: 0px; - border-bottom-left-radius: 0px; - border-bottom-right-radius: 0px; - } - } - .chat-inbox-row-bottom-pinned { - .chat-inbox-row { - border-top-left-radius: 0px; - border-top-right-radius: 0px; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - } - } -`; - -const InboxBodyHeaderContainer = styled(Flex)<{ isStandaloneChat?: boolean }>` - align-items: center; - z-index: 1; - padding: 0 0 8px 4px; - - ${({ isStandaloneChat }) => - isStandaloneChat && - ` - height: 58px; - padding: 12px - `} -`; +import { InboxBodyHeaderContainer } from './InboxBody.styles'; +import { InboxBodyList } from './InboxBodyList'; +import { InboxBodyLoadingHeader } from './InboxBodyLoadingHeader'; type Props = { inboxes: ChatModelType[]; accountIdentity: string | undefined; spacePath: string | undefined; + isLoading: boolean; isStandaloneChat?: boolean; isChatPinned: (path: string) => boolean; onClickInbox: (path: string) => void; @@ -70,6 +25,7 @@ export const InboxBody = ({ inboxes, accountIdentity, spacePath, + isLoading, isStandaloneChat, isChatPinned, onClickInbox, @@ -91,16 +47,7 @@ export const InboxBody = ({ const filteredInboxes = inboxes.filter(searchFilter); return ( - + @@ -138,77 +85,16 @@ export const InboxBody = ({ - {filteredInboxes.length === 0 ? ( - - - No chats yet. - - - Click the + to start. - - - ) : ( - - { - let lastIsPinned = false; - let nextIsPinned = false; - if (index > 0) { - const prevInbox = filteredInboxes[index - 1]; - if (isChatPinned(prevInbox.path)) { - lastIsPinned = true; - } - } - if (index < filteredInboxes.length - 1) { - const nextInbox = filteredInboxes[index + 1]; - if (isChatPinned(nextInbox.path)) { - nextIsPinned = true; - } - } - - return ( - - ); - }} - /> - - )} + {isLoading && } + ); }; diff --git a/app/src/renderer/apps/Courier/views/Inbox/InboxBodyList.tsx b/app/src/renderer/apps/Courier/views/Inbox/InboxBodyList.tsx new file mode 100644 index 0000000000..601b49a60b --- /dev/null +++ b/app/src/renderer/apps/Courier/views/Inbox/InboxBodyList.tsx @@ -0,0 +1,96 @@ +import { Flex, Text, WindowedList } from '@holium/design-system/general'; + +import { ChatModelType } from 'renderer/stores/models/chat.model'; + +import { InboxListContainer } from './InboxBody.styles'; +import { InboxRow } from './InboxRow'; + +type Props = { + inboxes: ChatModelType[]; + spacePath: string | undefined; + isEmpty: boolean; + isStandaloneChat: boolean; + accountIdentity: string | undefined; + isChatPinned: (path: string) => boolean; + onClickInbox: (path: string) => void; +}; + +export const InboxBodyList = ({ + inboxes, + isEmpty, + isStandaloneChat, + spacePath, + accountIdentity, + isChatPinned, + onClickInbox, +}: Props) => { + if (isEmpty) { + return ( + + + No chats yet. + + + Click the + to start. + + + ); + } + + return ( + + { + let lastIsPinned = false; + let nextIsPinned = false; + if (index > 0) { + const prevInbox = inboxes[index - 1]; + if (isChatPinned(prevInbox.path)) { + lastIsPinned = true; + } + } + if (index < inboxes.length - 1) { + const nextInbox = inboxes[index + 1]; + if (isChatPinned(nextInbox.path)) { + nextIsPinned = true; + } + } + + return ( + + ); + }} + /> + + ); +}; diff --git a/app/src/renderer/apps/Courier/views/Inbox/InboxBodyLoadingHeader.tsx b/app/src/renderer/apps/Courier/views/Inbox/InboxBodyLoadingHeader.tsx new file mode 100644 index 0000000000..b22bb393ab --- /dev/null +++ b/app/src/renderer/apps/Courier/views/Inbox/InboxBodyLoadingHeader.tsx @@ -0,0 +1,24 @@ +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +import { Spinner, Text } from '@holium/design-system/general'; + +const Container = styled(motion.div)` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 54px; + gap: 16px; + background-color: rgba(var(--rlm-accent-rgba), 0.1); +`; + +export const InboxBodyLoadingHeader = () => ( + + + Loading new messages... + +); diff --git a/app/src/renderer/stores/chat.store.ts b/app/src/renderer/stores/chat.store.ts index 6efd695fbe..93439fd7bf 100644 --- a/app/src/renderer/stores/chat.store.ts +++ b/app/src/renderer/stores/chat.store.ts @@ -132,6 +132,7 @@ export const ChatStore = types .actions((self) => ({ init: flow(function* () { try { + self.loader.set('loading'); const pinnedChats = yield ChatIPC.fetchPinnedChats(); const muted = yield ChatIPC.fetchMuted(); self.inbox = yield ChatIPC.getChatList(); @@ -140,11 +141,13 @@ export const ChatStore = types chat.setPinned(pinnedChats.includes(chat.path)); }); self.pinnedChats = pinnedChats; - return self.pinnedChats; } catch (error) { console.error(error); - return self.pinnedChats; } + + self.loader.set('loaded'); + + return self.inbox; }), fetchInboxMetadata: flow(function* () { const { muted, pinned } = yield ChatIPC.fetchPathMetadata(); @@ -152,11 +155,15 @@ export const ChatStore = types self.mutedChats = muted; }), loadChatList: flow(function* () { + self.loader.set('loading'); + try { self.inbox = yield ChatIPC.getChatList(); } catch (error) { console.error(error); } + + self.loader.set('loaded'); }), findChatDM: flow(function* (peer: string, our: string) { // find the DM, if exists, where it's only ourselves and the peer @@ -326,8 +333,9 @@ ChatIPC.onUpdate(({ type, payload }: ChatUpdateTypes) => { ); if (selectedChat) selectedChat.addMessage(payload); - if (shipStore.chatStore.subroute === 'inbox') + if (shipStore.chatStore.subroute === 'inbox') { shipStore.chatStore.refreshInbox(); + } return; } diff --git a/lib/design-system/blocks.ts b/lib/design-system/blocks.ts index 1b5e9b40b5..ab6c9439b8 100644 --- a/lib/design-system/blocks.ts +++ b/lib/design-system/blocks.ts @@ -5,7 +5,7 @@ export * from './src/blocks/Bubble/Bubble.styles'; export * from './src/blocks/Bubble/Bubble.types'; export { PinnedMessage } from './src/blocks/Bubble/Pinned'; export type { OnReactionPayload } from './src/blocks/Bubble/Reaction'; -export { ReactionPicker, Reactions } from './src/blocks/Bubble/Reaction'; +export { Reactions } from './src/blocks/Bubble/Reaction'; export { Reply } from './src/blocks/Bubble/Reply'; export * from './src/blocks/ChatInput/ChatInput'; export { diff --git a/lib/design-system/src/blocks/Bubble/Bubble.tsx b/lib/design-system/src/blocks/Bubble/Bubble.tsx index c8f9f30a33..491b59a0e9 100644 --- a/lib/design-system/src/blocks/Bubble/Bubble.tsx +++ b/lib/design-system/src/blocks/Bubble/Bubble.tsx @@ -30,6 +30,7 @@ export type BubbleProps = { authorNickname?: string; isEdited?: boolean; isEditing?: boolean; + isDeleting?: boolean; expiresAt?: number | null; updatedAt?: number | null; sentAt: string; @@ -60,6 +61,7 @@ export const Bubble = ({ message, isEdited, isEditing, + isDeleting, reactions = [], isPrevGrouped, isNextGrouped, @@ -144,118 +146,87 @@ export const Bubble = ({ const minBubbleWidth = useMemo(() => (isEdited ? 164 : 114), [isEdited]); - const reactionsDisplay = useMemo(() => { - return ( - - ); - }, [reactions.length, isOur, ourShip, ourColor, onReaction]); + if (message?.length === 1) { + const contentType = Object.keys(message[0])[0]; + if (contentType === 'status') { + return ( + + + + ); + } + } - return useMemo(() => { - if (message?.length === 1) { - const contentType = Object.keys(message[0])[0]; - if (contentType === 'status') { - return ( - + + {!isOur && !isPrevGrouped && ( + - + )} + {fragments} + + + - - ); - } - } - return ( - - - {!isOur && !isPrevGrouped && ( - - {authorNickname || author} - - )} - {fragments} - - - {reactionsDisplay} - - - {error && ( - - {error} - - )} - {expiresAt && ( - // TODO tooltip with time remaining - - )} + + + {error && ( - {isEditing && 'Editing... · '} - {isEdited && !isEditing && 'Edited · '} - {dateDisplay} + {error} - - - - - ); - }, [ - id, - isPrevGrouped, - isNextGrouped, - isOur, - ourColor, - isEditing, - isEdited, - authorColorDisplay, - authorNickname, - author, - fragments, - reactionsDisplay, - dateDisplay, - minBubbleWidth, - footerHeight, - error, - ]); + )} + {expiresAt && ( + // TODO tooltip with time remaining + + )} + + {isEditing && 'Editing... · '} + {isEdited && !isEditing && 'Edited · '} + {dateDisplay} + + + + + + ); }; diff --git a/lib/design-system/src/blocks/Bubble/Reaction.sizes.ts b/lib/design-system/src/blocks/Bubble/Reaction.sizes.ts new file mode 100644 index 0000000000..8b9239b20a --- /dev/null +++ b/lib/design-system/src/blocks/Bubble/Reaction.sizes.ts @@ -0,0 +1,20 @@ +export const REACTION_WIDTH = 300; +export const REACTION_HEIGHT = 350; + +export const ReactionSizes: Record = { + small: 16, + medium: 24, + large: 28, +}; + +export const EmojiSizes: Record = { + small: 10, + medium: 16, + large: 18, +}; + +export const FontSizes: Record = { + small: 12, + medium: 12, + large: 14, +}; diff --git a/lib/design-system/src/blocks/Bubble/Reaction.stories.tsx b/lib/design-system/src/blocks/Bubble/Reaction.stories.tsx index a1f24eb0fd..d3d70340c4 100644 --- a/lib/design-system/src/blocks/Bubble/Reaction.stories.tsx +++ b/lib/design-system/src/blocks/Bubble/Reaction.stories.tsx @@ -3,12 +3,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import { Box, Flex } from '../../../general'; import { FragmentReactionType } from './Bubble.types'; -import { - OnReactionPayload, - ReactionPicker, - ReactionPickerStyle, - Reactions, -} from './Reaction'; +import { OnReactionPayload, Reactions } from './Reaction'; export default { component: Reactions, @@ -42,15 +37,3 @@ export const Default: ComponentStory = () => { ); }; - -export const Picker: ComponentStory = () => { - return ( - - console.log(react)} - /> - - ); -}; diff --git a/lib/design-system/src/blocks/Bubble/Reaction.styles.ts b/lib/design-system/src/blocks/Bubble/Reaction.styles.ts new file mode 100644 index 0000000000..0864a76872 --- /dev/null +++ b/lib/design-system/src/blocks/Bubble/Reaction.styles.ts @@ -0,0 +1,136 @@ +import styled, { css } from 'styled-components'; + +import { Box, Text } from '../../../general'; +import { opacifyHexColor } from '../../util/colors'; +import { FontSizes, REACTION_WIDTH, ReactionSizes } from './Reaction.sizes'; + +export const ReactionContainer = styled(Box)<{ variant: 'overlay' | 'inline' }>` + display: flex; + position: relative; + flex-direction: row; + width: 100%; + max-width: ${REACTION_WIDTH}px; + flex-wrap: wrap; + gap: 2px; + z-index: 15; + .emoji-picker-menu { + &:hover { + .bubble-reactions { + transition: var(--transition); + opacity: 1; + } + } + } + ${({ variant }) => + variant === 'overlay' + ? css` + position: absolute; + left: 0px; + bottom: -8px; + ` + : css` + flex-direction: row; + `} +`; + +type ReactionButtonProps = { + hasCount?: boolean; + isOur?: boolean; + size?: keyof typeof ReactionSizes; + ourColor?: string; + selected?: boolean; +}; + +export const ReactionButton = styled(Box)` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + color: rgba(var(--rlm-text-rgba)); + background: ${({ selected, ourColor }) => + selected + ? ourColor + ? opacifyHexColor(ourColor, 0.3) + : 'gba(var(--rlm-accent-rgba))' + : 'rgba(0, 0, 0, 0.08)'}; + box-shadow: ${({ selected }) => + selected + ? 'inset 0px 0px 0px 1px rgba(0, 0, 0, 0.1)' + : 'inset 0px 0px 0px 1px rgba(0, 0, 0, 0.15)'}; + + /* TODO merged from master */ + /* background: ${({ selected }) => + selected ? 'rgba(var(--rlm-accent-rgba))' : 'rgba(var(--rlm-input-rgba))'}; + filter: ${({ selected }) => (selected ? 'brightness(1.3)' : 'brightness(1)')}; + border: ${({ selected }) => + selected + ? '1px solid rgba(var(--rlm-accent-rgba))' + : '1px solid rgba(var(--rlm-window-rgba))'}; */ + + border-radius: 16px; + transition: var(--transition); + ${({ size, selected, isOur }) => + size + ? css` + min-width: ${ReactionSizes[size]}px; + height: ${ReactionSizes[size]}px; + ${Text.Hint} { + font-size: ${FontSizes[size]}px; + ${selected && !isOur && 'color: rgba(var(--rlm-text-rgba));'} + ${selected && isOur && 'color: #FFF;'} + /* ${selected && 'color: rgba(var(--rlm-accent-rgba));'} */ + } + ` + : css` + min-width: 24px; + height: 24px; + `} + + width: auto; + img { + user-select: none; + pointer-events: none; + } + div { + user-select: none; + pointer-events: none; + } + ${({ hasCount, size }: ReactionButtonProps) => + hasCount && + size && + css` + min-width: ${ReactionSizes[size]}px; + transition: 0.01s ease-in-out; + padding: 0 6px 0 4px; + gap: 4px; + `} + + &:hover { + transition: var(--transition); + cursor: pointer; + filter: brightness(0.96); + } + ${({ isOur, ourColor, selected }) => + isOur && + ourColor && + css` + background: ${ourColor}; + filter: brightness(0.9); + border-color: rgba(var(--rlm-accent-rgba)); + transition: var(--transition); + &:hover { + transition: var(--transition); + filter: brightness(0.875); + } + + ${ + selected && + css` + filter: brightness(0.8); + &:hover { + filter: brightness(0.775); + } + ` + }} + `} +`; diff --git a/lib/design-system/src/blocks/Bubble/Reaction.tsx b/lib/design-system/src/blocks/Bubble/Reaction.tsx index 73b95d68cd..abd525be07 100644 --- a/lib/design-system/src/blocks/Bubble/Reaction.tsx +++ b/lib/design-system/src/blocks/Bubble/Reaction.tsx @@ -1,173 +1,20 @@ import { useCallback, useMemo } from 'react'; -import EmojiPicker, { - Emoji, - EmojiClickData, - EmojiStyle, - SkinTones, -} from 'emoji-picker-react'; -import { AnimatePresence } from 'framer-motion'; -import styled, { css } from 'styled-components'; -import { Box, Card, Flex, Icon, Portal, Text } from '../../../general'; import { useMenu } from '../../navigation/Menu/useMenu'; -import { opacifyHexColor } from '../../util/colors'; import { FragmentReactionType } from './Bubble.types'; +import { + REACTION_HEIGHT, + REACTION_WIDTH, + ReactionSizes, +} from './Reaction.sizes'; +import { ReactionContainer } from './Reaction.styles'; +import { ReactionCount } from './ReactionCount'; +import { ReactionRow } from './ReactionRow'; -const WIDTH = 300; -const HEIGHT = 350; const defaultShip = typeof window !== 'undefined' ? (window as any)?.ship ?? 'zod' : 'zod'; -const ReactionRow = styled(Box)<{ variant: 'overlay' | 'inline' }>` - display: flex; - position: relative; - flex-direction: row; - width: 100%; - max-width: ${WIDTH}px; - flex-wrap: wrap; - gap: 2px; - z-index: 15; - .emoji-picker-menu { - &:hover { - .bubble-reactions { - transition: var(--transition); - opacity: 1; - } - } - } - ${({ variant }) => - variant === 'overlay' - ? css` - position: absolute; - left: 0px; - bottom: -8px; - ` - : css` - flex-direction: row; - `} -`; - -const ReactionSizes: { [key: string]: number } = { - small: 16, - medium: 24, - large: 28, -}; - -const EmojiSizes: { [key: string]: number } = { - small: 10, - medium: 16, - large: 18, -}; - -const FontSizes: { [key: string]: number } = { - small: 12, - medium: 12, - large: 14, -}; - -type ReactionButtonProps = { - hasCount?: boolean; - isOur?: boolean; - size?: keyof typeof ReactionSizes; - ourColor?: string; - selected?: boolean; -}; - -export const ReactionButton = styled(Box)` - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - color: rgba(var(--rlm-text-rgba)); - background: ${({ selected, ourColor }) => - selected - ? ourColor - ? opacifyHexColor(ourColor, 0.3) - : 'gba(var(--rlm-accent-rgba))' - : 'rgba(0, 0, 0, 0.08)'}; - box-shadow: ${({ selected }) => - selected - ? 'inset 0px 0px 0px 1px rgba(0, 0, 0, 0.1)' - : 'inset 0px 0px 0px 1px rgba(0, 0, 0, 0.15)'}; - - /* TODO merged from master */ - /* background: ${({ selected }) => - selected ? 'rgba(var(--rlm-accent-rgba))' : 'rgba(var(--rlm-input-rgba))'}; - filter: ${({ selected }) => (selected ? 'brightness(1.3)' : 'brightness(1)')}; - border: ${({ selected }) => - selected - ? '1px solid rgba(var(--rlm-accent-rgba))' - : '1px solid rgba(var(--rlm-window-rgba))'}; */ - - border-radius: 16px; - transition: var(--transition); - ${({ size, selected, isOur }) => - size - ? css` - min-width: ${ReactionSizes[size]}px; - height: ${ReactionSizes[size]}px; - ${Text.Hint} { - font-size: ${FontSizes[size]}px; - ${selected && !isOur && 'color: rgba(var(--rlm-text-rgba));'} - ${selected && isOur && 'color: #FFF;'} - /* ${selected && 'color: rgba(var(--rlm-accent-rgba));'} */ - } - ` - : css` - min-width: 24px; - height: 24px; - `} - - width: auto; - img { - user-select: none; - pointer-events: none; - } - div { - user-select: none; - pointer-events: none; - } - ${({ hasCount, size }: ReactionButtonProps) => - hasCount && - size && - css` - min-width: ${ReactionSizes[size]}px; - transition: 0.01s ease-in-out; - padding: 0 6px 0 4px; - gap: 4px; - `} - - &:hover { - transition: var(--transition); - cursor: pointer; - filter: brightness(0.96); - } - ${({ isOur, ourColor, selected }) => - isOur && - ourColor && - css` - background: ${ourColor}; - filter: brightness(0.9); - border-color: rgba(var(--rlm-accent-rgba)); - transition: var(--transition); - &:hover { - transition: var(--transition); - filter: brightness(0.875); - } - - ${ - selected && - css` - filter: brightness(0.8); - &:hover { - filter: brightness(0.775); - } - ` - }} - `} -`; - -export type ReactionAggregateType = { +type ReactionAggregateType = { emoji: string; count: number; by: string[]; @@ -201,18 +48,17 @@ export const Reactions = ({ reactions = [], onReaction, }: ReactionProps) => { - const reactIds = reactions.map((r) => r.msgId); const { isOpen, menuRef, position, toggleMenu, closeMenu } = useMenu( 'top-left', - { width: WIDTH, height: HEIGHT }, - { x: 0, y: 2 }, - [], // closeableIds - [] // closeableClasses + { width: REACTION_WIDTH, height: REACTION_HEIGHT }, + { x: 0, y: 2 } ); + const reactionsAggregated = useMemo(() => { if (reactions.length === 0) { return []; } + return Object.values( reactions.reduce((acc, reaction) => { if (acc[reaction.emoji]) { @@ -228,22 +74,26 @@ export const Reactions = ({ return acc; }, {} as Record) ).sort((a, b) => b.count - a.count); - }, [reactIds, reactions.length]); + }, [reactions.length]); - const checkDupe = (emoji: string) => { - const index = reactionsAggregated.findIndex((r) => r.emoji === emoji); - if (index > -1) { - const reaction = reactionsAggregated[index]; - if (reaction.by.includes(ourShip)) { - return true; + const checkDupe = useCallback( + (emoji: string) => { + const index = reactionsAggregated.findIndex((r) => r.emoji === emoji); + if (index > -1) { + const reaction = reactionsAggregated[index]; + if (reaction.by.includes(ourShip)) { + return true; + } } - } - return false; - }; + return false; + }, + [reactionsAggregated] + ); - const onClick = useCallback( + const onClickEmoji = useCallback( (emoji: string) => { if (!onReaction) return; + if (!emoji) return; closeMenu(); if (checkDupe(emoji)) { @@ -261,218 +111,48 @@ export const Reactions = ({ onReaction({ emoji, action: 'add', by: ourShip }); } }, - [reactionsAggregated, ourShip, onReaction] + [checkDupe, closeMenu, onReaction, reactions] ); - // if we have reactions, and we're in overlay mode, switch to inline - const variantAuto = useMemo( - () => - reactions.length > 0 && variant === 'overlay' ? 'inline' : 'overlay', - [reactions.length, variant] - ); - - const memoizedRow = useMemo(() => { - return reactionsAggregated.map((reaction: ReactionAggregateType, index) => { - const selected = reaction.by.includes(ourShip); - return ( - 1} - onClick={(evt) => { - evt.stopPropagation(); - onClick(reaction.emoji); - }} - selected={selected} - > - - {reaction.count > 1 && ( - - {reaction.count} - - )} - - ); - }); - }, [reactionsAggregated, isOur, ourColor, size, onClick, ourShip]); - return ( - 0 && variant === 'overlay' ? 'inline' : 'overlay' + } onClick={(evt) => { evt.stopPropagation(); }} > - {memoizedRow} - <> - ( + { - toggleMenu(evt); - }} - > - - - - - {isOpen && position && ( - - { - evt.stopPropagation(); - }} - > - { - onClick(emojiData.unified); - evt.stopPropagation(); - }} - autoFocusSearch - /> - - - )} - - - - - ); -}; - -type ReactionPickerProps = { - isReacting: boolean; - anchorPoint: { x: number; y: number } | null; - onClick: (emoji: string) => void; -}; - -export const ReactionPickerStyle = styled(Flex)` - .EmojiPickerReact { - --epr-category-label-height: 22px; - --epr-category-navigation-button-size: 24px; - --epr-emoji-size: 24px; - --epr-search-input-height: 34px; - font-size: 12px; - } - - .epr-header-overlay { - padding: 8px !important; - } - .epr-skin-tones { - padding-left: 0px !important; - padding-right: 2px !important; - } - .epr-category-nav { - padding: 0px 8px !important; - padding-bottom: 4px !important; - --epr-category-navigation-button-size: 24px; - } - .ul.epr-emoji-list { - padding-bottom: 8px !important; - } - .epr-category-nav > button.epr-cat-btn:focus:before { - top: -3px; - left: -3px; - right: -3px; - bottom: -3px; - } -`; - -export const ReactionPicker = ({ - isReacting, - anchorPoint, - onClick, -}: ReactionPickerProps) => { - return ( - - { - evt.stopPropagation(); - // evt.preventDefault(); - onClick(emojiData.unified); - }} - autoFocusSearch={false} - /> - + onClick={onClickEmoji} + /> + ))} + {!isOur && ( + + )} + ); }; diff --git a/lib/design-system/src/blocks/Bubble/ReactionCount.tsx b/lib/design-system/src/blocks/Bubble/ReactionCount.tsx new file mode 100644 index 0000000000..47739e26ac --- /dev/null +++ b/lib/design-system/src/blocks/Bubble/ReactionCount.tsx @@ -0,0 +1,55 @@ +import { Emoji, EmojiStyle } from 'emoji-picker-react'; + +import { Text } from '../../../general'; +import { EmojiSizes } from './Reaction.sizes'; +import { ReactionButton } from './Reaction.styles'; + +type Props = { + id: string; + reaction: any; + isOur: boolean; + ourColor: string | undefined; + size: string; + isSelected: boolean; + onClick: (emoji: string) => void; +}; + +export const ReactionCount = ({ + id, + reaction, + isOur, + ourColor, + size, + isSelected, + onClick, +}: Props) => ( + 1} + onClick={(evt) => { + evt.stopPropagation(); + onClick(reaction.emoji); + }} + selected={isSelected} + > + + {reaction.count > 1 && ( + + {reaction.count} + + )} + +); diff --git a/lib/design-system/src/blocks/Bubble/ReactionEmojiPicker.styles.ts b/lib/design-system/src/blocks/Bubble/ReactionEmojiPicker.styles.ts new file mode 100644 index 0000000000..c5397fc685 --- /dev/null +++ b/lib/design-system/src/blocks/Bubble/ReactionEmojiPicker.styles.ts @@ -0,0 +1,35 @@ +import styled from 'styled-components'; + +import { Flex } from '../../general/Flex/Flex'; + +export const ReactionEmojiPickerStyle = styled(Flex)` + .EmojiPickerReact { + --epr-category-label-height: 22px; + --epr-category-navigation-button-size: 24px; + --epr-emoji-size: 24px; + --epr-search-input-height: 34px; + font-size: 12px; + } + + .epr-header-overlay { + padding: 8px !important; + } + .epr-skin-tones { + padding-left: 0px !important; + padding-right: 2px !important; + } + .epr-category-nav { + padding: 0px 8px !important; + padding-bottom: 4px !important; + --epr-category-navigation-button-size: 24px; + } + .ul.epr-emoji-list { + padding-bottom: 8px !important; + } + .epr-category-nav > button.epr-cat-btn:focus:before { + top: -3px; + left: -3px; + right: -3px; + bottom: -3px; + } +`; diff --git a/lib/design-system/src/blocks/Bubble/ReactionEmojiPicker.tsx b/lib/design-system/src/blocks/Bubble/ReactionEmojiPicker.tsx new file mode 100644 index 0000000000..1598d2a5cb --- /dev/null +++ b/lib/design-system/src/blocks/Bubble/ReactionEmojiPicker.tsx @@ -0,0 +1,87 @@ +import EmojiPicker, { EmojiClickData, SkinTones } from 'emoji-picker-react'; +import { AnimatePresence } from 'framer-motion'; + +import { Card, Portal } from '../../../general'; +import { REACTION_HEIGHT, REACTION_WIDTH } from './Reaction.sizes'; +import { ReactionEmojiPickerStyle } from './ReactionEmojiPicker.styles'; + +type Props = { + isOpen: boolean; + position: { x: number; y: number } | null; + menuRef: React.RefObject; + onClickEmoji: (emoji: string) => void; +}; + +export const ReactionEmojiPicker = ({ + isOpen, + position, + menuRef, + onClickEmoji, +}: Props) => { + if (!isOpen || !position) return null; + + return ( + + + { + evt.stopPropagation(); + }} + > + { + evt.stopPropagation(); + }} + > + { + evt.stopPropagation(); + onClickEmoji(emojiData.unified); + }} + autoFocusSearch + /> + + + + + ); +}; diff --git a/lib/design-system/src/blocks/Bubble/ReactionRow.tsx b/lib/design-system/src/blocks/Bubble/ReactionRow.tsx new file mode 100644 index 0000000000..0476d307d4 --- /dev/null +++ b/lib/design-system/src/blocks/Bubble/ReactionRow.tsx @@ -0,0 +1,55 @@ +import { RefObject, useMemo } from 'react'; + +import { Icon } from '../../../general'; +import { ReactionButton } from './Reaction.styles'; +import { ReactionEmojiPicker } from './ReactionEmojiPicker'; + +type Props = { + id: string; + isOur: boolean; + ourColor: string | undefined; + size: string; + isOpen: boolean; + menuRef: RefObject; + position: { x: number; y: number }; + toggleMenu: (event: React.MouseEvent) => void; + onClickEmoji: (emoji: string) => void; +}; + +export const ReactionRow = ({ + id, + isOur, + ourColor, + size, + isOpen, + menuRef, + position, + toggleMenu, + onClickEmoji, +}: Props) => { + return useMemo( + () => ( + <> + { + toggleMenu(evt); + }} + > + + + + + ), + [id, isOur, ourColor, size, menuRef, isOpen, position] + ); +}; diff --git a/lib/design-system/src/blocks/LinkBlock/TweetBlock.tsx b/lib/design-system/src/blocks/LinkBlock/TweetBlock.tsx index 7658dc5d88..64e2e898da 100644 --- a/lib/design-system/src/blocks/LinkBlock/TweetBlock.tsx +++ b/lib/design-system/src/blocks/LinkBlock/TweetBlock.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { createRoot } from 'react-dom/client'; import styled, { css } from 'styled-components'; @@ -34,8 +34,14 @@ type TweetBlockProps = { onTweetLoad?: () => void; } & BlockProps; -export const TweetBlock: FC = (props: TweetBlockProps) => { - const { id, link, width, height, onTweetLoad, ...rest } = props; +export const TweetBlock = ({ + id, + link, + width, + height, + onTweetLoad, + ...rest +}: TweetBlockProps) => { let tweetEmbed: any = null; // todo: get theme mode from context const themeMode = 'light'; @@ -60,18 +66,17 @@ export const TweetBlock: FC = (props: TweetBlockProps) => { return useMemo(() => { if (link.includes('status')) { const tweetId = link.split('status/')[1].split('?')[0]; - const tWidth = width; tweetEmbed = ( ); @@ -89,7 +94,7 @@ export const TweetBlock: FC = (props: TweetBlockProps) => { export const measureTweet = ( src: string, - _containerWidth?: number + width = 320 ): Promise<{ width: string; height: string }> => { const div = document.createElement('div'); const body = document.getElementsByTagName('body')[0]; @@ -100,11 +105,11 @@ export const measureTweet = ( div.style.position = 'absolute'; div.style.top = '-1000px'; div.style.left = '-1000px'; - div.style.width = `${320}px`; + div.style.width = `${width}px`; div.style.padding = '8px'; div.style.boxSizing = 'border-box'; div.style.minWidth = '150px'; - div.style.height = '700px'; + div.style.height = '340px'; body.appendChild(div); const root = createRoot(div); @@ -114,7 +119,7 @@ export const measureTweet = ( id="premeasure-tweet" variant="content" link={src} - width={320} + width={width} height={340} onTweetLoad={() => { const webview: HTMLWebViewElement | null = document.getElementById( diff --git a/lib/design-system/src/blocks/MediaBlock/MediaBlock.tsx b/lib/design-system/src/blocks/MediaBlock/MediaBlock.tsx index eb0148ce65..8ee839d5a3 100644 --- a/lib/design-system/src/blocks/MediaBlock/MediaBlock.tsx +++ b/lib/design-system/src/blocks/MediaBlock/MediaBlock.tsx @@ -107,7 +107,7 @@ export const MediaBlock = ({ id={rest.id} url={url} controls - className={'react-player-iframe'} + className="react-player-hide-cursor" onReady={() => { setIsReady(true); onLoaded && onLoaded(); @@ -120,7 +120,6 @@ export const MediaBlock = ({ style={{ borderRadius: '4px', overflow: 'hidden', - cursor: 'none', }} config={{ youtube: { @@ -147,110 +146,4 @@ const MediaWrapper = styled(Flex)` height: fit-content; width: 100%; min-width: 250px; - .react-player-hide-cursor { - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; - cursor: none !important; - z-index: 100; - pointer-events: auto; - } - - video::-webkit-media-controls-panel { - cursor: none !important; - } - - video::-webkit-media-controls-play-button { - cursor: none !important; - } - - video::-webkit-media-controls-volume-slider-container { - cursor: none !important; - } - - video::-webkit-media-controls-volume-slider { - cursor: none !important; - } - - video::-webkit-media-controls-mute-button { - cursor: none !important; - } - - video::-webkit-media-controls-timeline { - cursor: none !important; - } - - video::-webkit-media-controls-current-time-display { - cursor: none !important; - } - - video::-webkit-full-page-media::-webkit-media-controls-panel { - cursor: none !important; - } - - video::-webkit-media-controls-panel { - cursor: none !important; - } - - video::-webkit-media-controls-start-playback-button { - cursor: none !important; - } - - video::-webkit-media-controls-overlay-play-button { - cursor: none !important; - } - - video::-webkit-media-controls-toggle-closed-captions-button { - cursor: none !important; - } - - video::-webkit-media-controls-status-display { - cursor: none !important; - } - - video::-webkit-media-controls-mouse-display { - cursor: none !important; - } - - video::-webkit-media-controls-timeline-container { - cursor: none !important; - } - - video::-webkit-media-controls-time-remaining-display { - cursor: none !important; - } - - video::-webkit-media-controls-seek-back-button { - cursor: none !important; - } - - video { - cursor: none !important; - } - - video::-webkit-media-controls-seek-forward-button { - cursor: none !important; - } - - video::-webkit-media-controls-fullscreen-button { - cursor: none !important; - } - - video::-webkit-media-controls-enclosure { - cursor: none !important; - } - - video::-webkit-media-controls-rewind-button { - cursor: none !important; - } - - video::-webkit-media-controls-return-to-realtime-button { - cursor: none !important; - } - - video::-webkit-media-controls-toggle-closed-captions-button { - cursor: none !important; - } `; diff --git a/lib/design-system/src/blocks/index.ts b/lib/design-system/src/blocks/index.ts deleted file mode 100644 index 846c794a54..0000000000 --- a/lib/design-system/src/blocks/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export type { BlockProps } from './Block/Block'; -export { Block, BlockStyle } from './Block/Block'; -export { Bubble } from './Bubble/Bubble'; -export * from './Bubble/Bubble.styles'; -export * from './Bubble/Bubble.types'; -export { PinnedMessage } from './Bubble/Pinned'; -export type { OnReactionPayload } from './Bubble/Reaction'; -export { ReactionPicker, Reactions } from './Bubble/Reaction'; -export { Reply } from './Bubble/Reply'; -export * from './ChatInput/ChatInput'; -export * from './ImageBlock/ImageBlock'; -export { measureImage } from './ImageBlock/measure'; -export * from './LinkBlock/LinkBlock'; -export { measureTweet } from './LinkBlock/TweetBlock'; -export * from './LinkBlock/util'; -export * from './MediaBlock/MediaBlock'; -export * from './MemeBlock/MemeBlock'; -export * from './TextBlock/TextBlock'; diff --git a/lib/design-system/src/general/Spinner/Spinner.styles.tsx b/lib/design-system/src/general/Spinner/Spinner.styles.tsx index c4092fa35f..d2807d8759 100644 --- a/lib/design-system/src/general/Spinner/Spinner.styles.tsx +++ b/lib/design-system/src/general/Spinner/Spinner.styles.tsx @@ -2,13 +2,18 @@ import styled from 'styled-components'; type Props = { size: number; + width?: number; color?: string; }; export const StyledSpinner = styled.div` width: ${({ size }) => size}px; height: ${({ size }) => size}px; - border-width: ${({ size }) => (size < 2 ? 0.75 : 5)}px; + border-width: ${({ size, width }) => { + if (width) return width; + + return size < 2 ? 0.75 : 5; + }}px; border-style: solid; // TODO: get brand color from a CSS variable. border-color: rgba(117, 117, 117, 0.2); diff --git a/lib/design-system/src/general/Spinner/Spinner.tsx b/lib/design-system/src/general/Spinner/Spinner.tsx index acc53c93e9..dc729a4dc1 100644 --- a/lib/design-system/src/general/Spinner/Spinner.tsx +++ b/lib/design-system/src/general/Spinner/Spinner.tsx @@ -14,16 +14,18 @@ const sizes = [12, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88]; type SpinnerProps = { size: number | string; + width?: number; color?: string; } & SpaceProps & LayoutProps & FlexboxProps & PositionProps; -export const Spinner = ({ size, color, ...boxProps }: SpinnerProps) => ( +export const Spinner = ({ size, color, width, ...boxProps }: SpinnerProps) => ( diff --git a/lib/design-system/src/navigation/Menu/useMenu.tsx b/lib/design-system/src/navigation/Menu/useMenu.tsx index 7791395520..7693879d1b 100644 --- a/lib/design-system/src/navigation/Menu/useMenu.tsx +++ b/lib/design-system/src/navigation/Menu/useMenu.tsx @@ -1,81 +1,4 @@ -// import { useState, useEffect, useRef } from 'react'; - -// interface MenuState { -// isOpen: boolean; -// anchorEl: null | HTMLElement; -// } - -// function useMenu(): { -// isOpen: boolean; -// anchorEl: null | HTMLElement; -// openMenu: (event: React.MouseEvent) => void; -// closeMenu: () => void; -// toggleMenu: (event: React.MouseEvent) => void; -// menuRef: React.RefObject; -// } { -// const [menuState, setMenuState] = useState({ -// isOpen: false, -// anchorEl: null, -// }); -// const menuRef = useRef(null); - -// useEffect(() => { -// const handleOutsideClick = (event: MouseEvent) => { -// if ( -// menuState.anchorEl && -// !menuState.anchorEl.contains(event.target as Node) -// ) { -// closeMenu(); -// } -// }; - -// if (menuState.isOpen) { -// const listener = (event: MouseEvent) => handleOutsideClick(event); -// document.addEventListener('mousedown', listener); -// return () => { -// document.removeEventListener('mousedown', listener); -// }; -// } -// }, [menuState.isOpen, menuState.anchorEl]); - -// const openMenu = (event: React.MouseEvent) => { -// if (!menuState.isOpen) { -// setMenuState({ -// isOpen: true, -// anchorEl: event.currentTarget, -// }); -// } -// }; - -// const closeMenu = () => { -// setMenuState({ -// isOpen: false, -// anchorEl: null, -// }); -// }; - -// const toggleMenu = (event: React.MouseEvent) => { -// if (menuState.isOpen) { -// closeMenu(); -// } else { -// openMenu(event); -// } -// }; - -// return { -// isOpen: menuState.isOpen, -// anchorEl: menuState.anchorEl, -// openMenu, -// closeMenu, -// toggleMenu, -// menuRef, -// }; -// } - -// export default useMenu; - -import { useEffect, useRef, useState } from 'react'; -import ReactDOM from 'react-dom'; +import { RefObject, useCallback, useEffect, useRef, useState } from 'react'; import { Dimensions, Position } from '../../../util'; import { getAnchorPointByTarget } from '../../util/position'; @@ -97,70 +20,71 @@ type Orientation = | 'bottom-right' | 'bottom'; +type UseMenu = { + isOpen: boolean; + anchorEl: null | HTMLElement; + position: { y: number; x: number }; + menuRef: RefObject; + openMenu: (event: React.MouseEvent) => void; + toggleMenu: (event: React.MouseEvent) => void; + closeMenu: () => void; +}; + export function useMenu( orientation: Orientation = 'bottom-left', menuDimensions: Dimensions, offset?: Position, closableIds?: string[], closableClasses?: string[] -): { - isOpen: boolean; - anchorEl: null | HTMLElement; - position: { y: number; x: number }; - openMenu: (event: React.MouseEvent) => void; - closeMenu: () => void; - toggleMenu: (event: React.MouseEvent) => void; - menuRef: React.RefObject; -} { +): UseMenu { + const menuRef = useRef(null); const [menuState, setMenuState] = useState({ isOpen: false, anchorEl: null, position: { y: 0, x: 0 }, }); - const menuRef = useRef(null); - useEffect(() => { - const handleOutsideClick = (event: MouseEvent) => { - // eslint-disable-next-line react/no-find-dom-node - const domNode = ReactDOM.findDOMNode(menuRef.current); + const handleOutsideClick = useCallback( + (event: MouseEvent) => { + event.stopPropagation(); + + const isMenuAnchorClick = + menuState.anchorEl && menuState.anchorEl.contains(event.target as Node); + const isMenuContainerClick = + menuRef.current && menuRef.current.contains(event.target as Node); - if (domNode && domNode.contains(event.target as HTMLElement)) { + if (isMenuAnchorClick) { + return; + } else if (isMenuContainerClick && menuState.isOpen) { // check if the click is on a closable element - if (closableIds) { + if (closableIds?.length) { const id = (event.target as HTMLElement).id; if (closableIds.includes(id)) { closeMenu(); return; } } - if (closableClasses) { + if (closableClasses?.length) { const classes = (event.target as HTMLElement).classList; if (closableClasses.some((c) => classes.contains(c))) { closeMenu(); return; } } - event.stopPropagation(); - } else { - if ( - menuState.anchorEl && - !menuState.anchorEl.contains(event.target as Node) - ) { - closeMenu(); - event.stopPropagation(); - } + } else if (menuState.isOpen) { + closeMenu(); } - }; + }, + [menuState, closableIds, closableClasses] + ); - if (menuState.isOpen) { - const listener = (event: MouseEvent) => handleOutsideClick(event); - document.addEventListener('mousedown', listener); - return () => { - document.removeEventListener('mousedown', listener); - }; - } - return; - }, [menuState.isOpen, menuState.anchorEl, closableIds, closableClasses]); + useEffect(() => { + document.addEventListener('mousedown', handleOutsideClick); + + return () => { + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, [handleOutsideClick]); const calculatePosition = ( anchorEvent: React.MouseEvent,