diff --git a/web/chat/chat-message-list.css b/web/chat/chat-message-list.css index 63a1ea4f1..55393707e 100644 --- a/web/chat/chat-message-list.css +++ b/web/chat/chat-message-list.css @@ -1,213 +1,215 @@ div.outerMessageContainer { position: relative; height: calc(100vh - 128px); min-height: 0; display: flex; flex-direction: column; } div.messageContainer { height: 100%; overflow-y: auto; display: flex; flex-direction: column-reverse; } div.mirroredMessageContainer { flex-direction: column !important; transform: scaleY(-1); } div.mirroredMessageContainer > div { transform: scaleY(-1); } div.message { display: flex; flex-direction: column; flex-shrink: 0; } div.loading { text-align: center; padding: 12px; } div.conversationHeader { color: var(--chat-timestamp-color); font-size: var(--xs-font-12); padding: 6px 0; line-height: var(--line-height-text); text-align: center; } div.conversationHeader:last-child { padding-top: 6px; } div.messageTooltipActiveArea { position: absolute; display: flex; top: 0; bottom: 0; align-items: center; padding: 0 12px; } div.viewerMessageTooltipActiveArea { right: 100%; } div.nonViewerMessageActiveArea { left: 100%; } div.messageTooltipActiveArea > div + div { margin-left: 4px; } div.messageTooltipLinkIcon:hover { cursor: pointer; } div.textMessage { padding: 6px 12px; white-space: pre-wrap; word-wrap: break-word; width: 100%; box-sizing: border-box; } div.textMessageDefaultBackground { background-color: var(--text-message-default-background); } div.normalTextMessage { font-size: 16px; } div.emojiOnlyTextMessage { font-size: 32px; font-family: emoji; } span.authorName { color: #777777; font-size: 14px; padding: 4px 24px; } div.darkTextMessage { color: white; } div.lightTextMessage { color: black; } div.content { display: flex; flex-shrink: 0; align-items: center; margin-bottom: 5px; box-sizing: border-box; width: 100%; } div.nonViewerContent { align-self: flex-start; justify-content: flex-start; padding-right: 8px; } div.viewerContent { align-self: flex-end; justify-content: flex-end; padding-right: 4px; } div.iconContainer { margin-right: 1px; } div.iconContainer > svg { height: 16px; } div.messageBoxContainer { position: relative; display: flex; max-width: calc(min(68%, 1000px)); margin: 0 4px 0 12px; } div.fixedWidthMessageBoxContainer { width: 68%; } div.messageBox { overflow: hidden; display: flex; flex-wrap: wrap; justify-content: space-between; flex-shrink: 0; max-width: 100%; } div.fixedWidthMessageBox { width: 100%; } div.failedSend { - text-transform: uppercase; display: flex; justify-content: flex-end; flex-shrink: 0; - font-size: 14px; margin-right: 30px; padding-bottom: 6px; } -span.deliveryFailed { + +.deliveryFailed { + text-transform: uppercase; + font-size: 14px; padding: 0 3px; - color: #555555; + color: var(--fg); } -a.retrySend { - padding: 0 3px; - cursor: pointer; + +.retryButtonText { + text-transform: uppercase; + font-size: 14px; } div.messageBox > div.imageGrid { display: grid; width: 100%; grid-template-columns: repeat(6, 1fr); grid-gap: 5px; } div.messageBox span.multimedia > span.multimediaImage { min-height: initial; min-width: initial; } div.messageBox span.multimedia > span.multimediaImage > img { max-height: 600px; } div.imageGrid > span.multimedia { grid-column-end: span 3; } div.imageGrid > span.multimedia:first-child { margin-top: 0; } div.imageGrid > span.multimedia > span.multimediaImage { flex: 1; } div.imageGrid > span.multimedia > span.multimediaImage:after { content: ''; display: block; padding-bottom: calc(min(600px, 100%)); } div.imageGrid > span.multimedia > span.multimediaImage > img { position: absolute; width: 100%; height: 100%; object-fit: cover; } div.imageGrid > span.multimedia:nth-last-child(n + 3):first-child, div.imageGrid > span.multimedia:nth-last-child(n + 3):first-child ~ * { grid-column-end: span 2; } div.imageGrid > span.multimedia:nth-last-child(n + 4):first-child, div.imageGrid > span.multimedia:nth-last-child(n + 4):first-child ~ * { grid-column-end: span 3; } div.imageGrid > span.multimedia:nth-last-child(n + 5):first-child, div.imageGrid > span.multimedia:nth-last-child(n + 5):first-child ~ * { grid-column-end: span 2; } div.sidebarMarginBottom { margin-bottom: 2px; } svg.inlineSidebarIcon { color: #666666; } diff --git a/web/chat/chat-thread-composer.css b/web/chat/chat-thread-composer.css index 33fbc77a1..ce5024400 100644 --- a/web/chat/chat-thread-composer.css +++ b/web/chat/chat-thread-composer.css @@ -1,72 +1,74 @@ div.threadSearchContainer { background-color: var(--thread-creation-search-container-bg); color: var(--fg); display: flex; flex-direction: column; max-height: 50%; overflow: auto; flex-shrink: 0; } div.fullHeight { flex-grow: 1; max-height: 100%; } div.userSelectedTags { display: flex; flex-wrap: wrap; flex-direction: row; align-items: center; gap: 4px; padding: 4px 12px; margin-bottom: 8px; } div.searchRow { display: flex; flex-direction: row; align-items: center; margin-right: 8px; } div.searchField { flex-grow: 1; } div.closeSearch { cursor: pointer; display: flex; align-items: center; color: var(--thread-creation-close-search-color); margin: 0 8px; } ul.searchResultsContainer { display: flex; flex-direction: column; overflow: auto; padding: 0 12px 8px; + list-style-type: none; } -li.searchResultsItem { +.searchResultsItem { display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - cursor: pointer; } -li.searchResultsItem:hover { +.searchResultsItem:hover { background-color: var(--thread-creation-search-item-bg-hover); } +.searchResultsButton { + justify-content: space-between; + flex: 1; + padding: 8px 12px; +} + div.userName { color: var(--fg); } div.userInfo { font-style: italic; color: var(--thread-creation-search-item-info-color); } diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js index fc00c6c5e..f121853f6 100644 --- a/web/chat/chat-thread-composer.react.js +++ b/web/chat/chat-thread-composer.react.js @@ -1,184 +1,187 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors'; import { getPotentialMemberItems } from 'lib/shared/search-utils'; import { threadIsPending } from 'lib/shared/thread-utils'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types'; +import Button from '../components/button.react'; import Label from '../components/label.react'; import Search from '../components/search.react'; import type { InputState } from '../input/input-state'; import { updateNavInfoActionType } from '../redux/action-types'; import { useSelector } from '../redux/redux-utils'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './chat-thread-composer.css'; type Props = { +userInfoInputArray: $ReadOnlyArray, +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, }; type ActiveThreadBehavior = | 'reset-active-thread-if-pending' | 'keep-active-thread'; function ChatThreadComposer(props: Props): React.Node { const { userInfoInputArray, otherUserInfos, threadID, inputState } = props; const [usernameInputText, setUsernameInputText] = React.useState(''); const dispatch = useDispatch(); const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), [userInfoInputArray], ); const userListItems = React.useMemo( () => getPotentialMemberItems( usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs, ), [usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs], ); const onSelectUserFromSearch = React.useCallback( (id: string) => { const selectedUserIDs = userInfoInputArray.map(user => user.id); dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: [...selectedUserIDs, id], }, }); setUsernameInputText(''); }, [dispatch, userInfoInputArray], ); const onRemoveUserFromSelected = React.useCallback( (id: string) => { const selectedUserIDs = userInfoInputArray.map(user => user.id); if (!selectedUserIDs.includes(id)) { return; } dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: selectedUserIDs.filter(userID => userID !== id), }, }); }, [dispatch, userInfoInputArray], ); const userSearchResultList = React.useMemo(() => { if ( !userListItems.length || (!usernameInputText && userInfoInputArray.length) ) { return null; } return ( ); }, [ onSelectUserFromSearch, userInfoInputArray.length, userListItems, usernameInputText, ]); const hideSearch = React.useCallback( (threadBehavior: ActiveThreadBehavior = 'keep-active-thread') => { dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', activeChatThreadID: threadBehavior === 'keep-active-thread' || !threadIsPending(threadID) ? threadID : null, }, }); }, [dispatch, threadID], ); const onCloseSearch = React.useCallback(() => { hideSearch('reset-active-thread-if-pending'); }, [hideSearch]); const tagsList = React.useMemo(() => { if (!userInfoInputArray?.length) { return null; } const labels = userInfoInputArray.map(user => { return ( ); }); return
{labels}
; }, [userInfoInputArray, onRemoveUserFromSelected]); React.useEffect(() => { if (!inputState) { return; } inputState.registerSendCallback(hideSearch); return () => inputState.unregisterSendCallback(hideSearch); }, [hideSearch, inputState]); const threadSearchContainerStyles = React.useMemo( () => classNames(css.threadSearchContainer, { [css.fullHeight]: !userInfoInputArray.length, }), [userInfoInputArray.length], ); return (
{tagsList} {userSearchResultList}
); } export default ChatThreadComposer; diff --git a/web/chat/failed-send.react.js b/web/chat/failed-send.react.js index 3a3d98343..f53d489c2 100644 --- a/web/chat/failed-send.react.js +++ b/web/chat/failed-send.react.js @@ -1,161 +1,157 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors'; import { messageID } from 'lib/shared/message-utils'; import { messageTypes, type RawComposableMessageInfo, assertComposableMessageType, } from 'lib/types/message-types'; import { type ThreadInfo } from 'lib/types/thread-types'; +import Button from '../components/button.react'; import { type InputState, InputStateContext } from '../input/input-state'; import { useSelector } from '../redux/redux-utils'; import css from './chat-message-list.css'; import multimediaMessageSendFailed from './multimedia-message-send-failed'; import textMessageSendFailed from './text-message-send-failed'; type BaseProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, }; type Props = { ...BaseProps, // Redux state +rawMessageInfo: RawComposableMessageInfo, // withInputState +inputState: ?InputState, }; class FailedSend extends React.PureComponent { retryingText = false; retryingMedia = false; componentDidUpdate(prevProps: Props) { if ( (this.props.rawMessageInfo.type === messageTypes.IMAGES || this.props.rawMessageInfo.type === messageTypes.MULTIMEDIA) && (prevProps.rawMessageInfo.type === messageTypes.IMAGES || prevProps.rawMessageInfo.type === messageTypes.MULTIMEDIA) ) { const { inputState } = this.props; const prevInputState = prevProps.inputState; invariant( inputState && prevInputState, 'inputState should be set in FailedSend', ); const isFailed = multimediaMessageSendFailed(this.props.item, inputState); const wasFailed = multimediaMessageSendFailed( prevProps.item, prevInputState, ); const isDone = this.props.item.messageInfo.id !== null && this.props.item.messageInfo.id !== undefined; const wasDone = prevProps.item.messageInfo.id !== null && prevProps.item.messageInfo.id !== undefined; if ((isFailed && !wasFailed) || (isDone && !wasDone)) { this.retryingMedia = false; } } else if ( this.props.rawMessageInfo.type === messageTypes.TEXT && prevProps.rawMessageInfo.type === messageTypes.TEXT ) { const isFailed = textMessageSendFailed(this.props.item); const wasFailed = textMessageSendFailed(prevProps.item); const isDone = this.props.item.messageInfo.id !== null && this.props.item.messageInfo.id !== undefined; const wasDone = prevProps.item.messageInfo.id !== null && prevProps.item.messageInfo.id !== undefined; if ((isFailed && !wasFailed) || (isDone && !wasDone)) { this.retryingText = false; } } } render() { - const threadColor = { - color: `#${this.props.threadInfo.color}`, - }; return (
- Delivery failed. - Delivery failed. +
); } - retrySend = (event: SyntheticEvent) => { - event.stopPropagation(); - + retrySend = () => { const { inputState } = this.props; invariant(inputState, 'inputState should be set in FailedSend'); const { rawMessageInfo } = this.props; if (rawMessageInfo.type === messageTypes.TEXT) { if (this.retryingText) { return; } this.retryingText = true; inputState.sendTextMessage( { ...rawMessageInfo, time: Date.now(), }, this.props.threadInfo, ); } else if ( rawMessageInfo.type === messageTypes.IMAGES || rawMessageInfo.type === messageTypes.MULTIMEDIA ) { const { localID } = rawMessageInfo; invariant(localID, 'failed RawMessageInfo should have localID'); if (this.retryingMedia) { return; } this.retryingMedia = true; inputState.retryMultimediaMessage(localID, this.props.threadInfo); } }; } const ConnectedFailedSend: React.ComponentType = React.memo( function ConnectedFailedSend(props) { const { messageInfo } = props.item; assertComposableMessageType(messageInfo.type); const id = messageID(messageInfo); const rawMessageInfo = useSelector( state => state.messageStore.messages[id], ); assertComposableMessageType(rawMessageInfo.type); invariant( rawMessageInfo.type === messageTypes.TEXT || rawMessageInfo.type === messageTypes.IMAGES || rawMessageInfo.type === messageTypes.MULTIMEDIA, 'FailedSend should only be used for composable message types', ); const inputState = React.useContext(InputStateContext); return ( ); }, ); export default ConnectedFailedSend; diff --git a/web/components/button.css b/web/components/button.css index 9b8381915..1d01f1597 100644 --- a/web/components/button.css +++ b/web/components/button.css @@ -1,71 +1,80 @@ .btn { --border-width: 1px; --border-radius: 4px; position: relative; display: flex; + flex-direction: row; align-items: center; justify-content: center; border: var(--border-width) solid transparent; font-size: var(--m-font-16); padding: 12px 24px; color: var(--fg); border-radius: var(--border-radius); cursor: pointer; } .btn.outline { border: var(--border-width) solid var(--btn-outline-border); } .btn > * { position: relative; } .btn::before { content: ''; border: inherit; border-radius: inherit; background: inherit; position: absolute; top: calc(-1 * var(--border-width)); left: calc(-1 * var(--border-width)); width: 100%; height: 100%; } .btn.outline::before { top: 0; left: 0; border: none; border-radius: calc(var(--border-radius) - var(--border-width)); } .btn:hover::before { transition-duration: 200ms; transition-property: filter; } .btn:hover:not(:disabled)::before { filter: brightness(0.8); } .btn.outline:hover:not(:disabled)::before { filter: brightness(2); } .btn:disabled { cursor: not-allowed; color: var(--btn-disabled-color); } .btn:not(.outline):disabled::before { background-color: var(--btn-bg-disabled); } .round { + background: var(--settings-btn-bg); width: 30px; height: 30px; border-radius: 50%; padding: 0; } + +.text { + background: transparent; + white-space: nowrap; + padding: 0; + border: none; +} diff --git a/web/components/button.react.js b/web/components/button.react.js index 1c7dbdafb..89087e84f 100644 --- a/web/components/button.react.js +++ b/web/components/button.react.js @@ -1,81 +1,81 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import css from './button.css'; -export type ButtonVariant = 'filled' | 'outline' | 'round'; +export type ButtonVariant = 'filled' | 'outline' | 'round' | 'text'; export type ButtonColor = { +backgroundColor?: string, +color?: string, }; export const buttonThemes: { [string]: ButtonColor } = { standard: { backgroundColor: 'var(--btn-bg-filled)', }, danger: { backgroundColor: 'var(--btn-bg-danger)', }, success: { backgroundColor: 'var(--btn-bg-success)', }, outline: { backgroundColor: 'var(--btn-bg-outline)', }, }; export type ButtonProps = { +onClick: (event: SyntheticEvent) => mixed, +children: React.Node, +variant?: ButtonVariant, +buttonColor?: ButtonColor, +type?: string, +disabled?: boolean, +className?: string, }; function Button(props: ButtonProps): React.Node { const { onClick, children, variant = 'filled', buttonColor, type, disabled = false, className = '', } = props; const btnCls = classnames(css.btn, css[variant]); - let style; + let style = {}; if (buttonColor) { style = buttonColor; } else if (variant === 'outline') { style = buttonThemes.outline; - } else { + } else if (variant === 'filled' || variant === 'round') { style = buttonThemes.standard; } const wrappedChildren = React.Children.map(children, child => { if (typeof child === 'string' || typeof child === 'number') { return {child}; } return child; }); return ( ); } export default Button; diff --git a/web/settings/account-settings.css b/web/settings/account-settings.css index ed59f8a0c..6c55443d9 100644 --- a/web/settings/account-settings.css +++ b/web/settings/account-settings.css @@ -1,66 +1,61 @@ .container { padding: 40px; width: 456px; } .header { color: var(--fg); font-weight: var(--semi-bold); line-height: var(--line-height-display); padding-bottom: 55px; } .content ul { list-style-type: none; } .content li { color: var(--account-settings-label); padding: 24px 16px 16px; display: flex; flex-direction: row; justify-content: space-between; } .content li:not(:last-child) { border-bottom: 1px solid var(--border-color); } .logoutContainer { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg); } .logoutLabel { color: var(--account-settings-label); } .username { color: var(--fg); } -.button { +.buttonText { color: var(--account-button-color); - background-color: transparent; - font-size: var(--m-font-16); - border: none; - cursor: pointer; line-height: var(--line-height-text); - white-space: nowrap; } .passwordContainer { display: flex; } .password { align-items: center; padding-right: 16px; } .editPasswordLink { color: var(--account-settings-label); cursor: pointer; } diff --git a/web/settings/account-settings.react.js b/web/settings/account-settings.react.js index ab84a6a6e..1ce056b69 100644 --- a/web/settings/account-settings.react.js +++ b/web/settings/account-settings.react.js @@ -1,99 +1,100 @@ // @flow import * as React from 'react'; import { logOut, logOutActionTypes } from 'lib/actions/user-actions'; import { useModalContext } from 'lib/components/modal-provider.react'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; +import Button from '../components/button.react'; import { useSelector } from '../redux/redux-utils'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './account-settings.css'; import PasswordChangeModal from './password-change-modal'; import BlockListModal from './relationship/block-list-modal.react'; import FriendListModal from './relationship/friend-list-modal.react'; function AccountSettings(): React.Node { const sendLogoutRequest = useServerCall(logOut); const preRequestUserState = useSelector(preRequestUserStateSelector); const dispatchActionPromise = useDispatchActionPromise(); const logOutUser = React.useCallback( () => dispatchActionPromise( logOutActionTypes, sendLogoutRequest(preRequestUserState), ), [dispatchActionPromise, preRequestUserState, sendLogoutRequest], ); const { pushModal, popModal } = useModalContext(); const showPasswordChangeModal = React.useCallback( () => pushModal(), [pushModal], ); const openFriendList = React.useCallback( () => pushModal(), [popModal, pushModal], ); const openBlockList = React.useCallback( () => pushModal(), [popModal, pushModal], ); const currentUserInfo = useSelector(state => state.currentUserInfo); if (!currentUserInfo || currentUserInfo.anonymous) { return null; } const { username } = currentUserInfo; return (

My Account

  • {'Logged in as '} {username}

    - +
  • Password ******
  • Friend List - +
  • Block List - +
); } export default AccountSettings; diff --git a/web/settings/relationship/friend-list-row.react.js b/web/settings/relationship/friend-list-row.react.js index a83c61c2c..1b0a8fef3 100644 --- a/web/settings/relationship/friend-list-row.react.js +++ b/web/settings/relationship/friend-list-row.react.js @@ -1,81 +1,89 @@ // @flow -import classnames from 'classnames'; import * as React from 'react'; import { useRelationshipCallbacks } from 'lib/hooks/relationship-prompt'; import { userRelationshipStatus } from 'lib/types/relationship-types'; +import Button from '../../components/button.react'; import MenuItem from '../../components/menu-item.react'; import Menu from '../../components/menu.react'; import SWMansionIcon from '../../SWMansionIcon.react'; import css from './user-list-row.css'; import type { UserRowProps } from './user-list.react'; +const dangerButtonColor = { + color: 'var(--btn-bg-danger)', +}; + function FriendListRow(props: UserRowProps): React.Node { const { userInfo, onMenuVisibilityChange } = props; const { friendUser, unfriendUser } = useRelationshipCallbacks(userInfo.id); const buttons = React.useMemo(() => { if (userInfo.relationshipStatus === userRelationshipStatus.REQUEST_SENT) { return ( - + ); } if ( userInfo.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED ) { return ( <> - - + ); } if (userInfo.relationshipStatus === userRelationshipStatus.FRIEND) { const editIcon = ; return (
); } return undefined; }, [ friendUser, unfriendUser, userInfo.relationshipStatus, onMenuVisibilityChange, ]); return (
{userInfo.username}
{buttons}
); } export default FriendListRow; diff --git a/web/settings/relationship/user-list-row.css b/web/settings/relationship/user-list-row.css index 27a9bdd4c..4a8bd4e90 100644 --- a/web/settings/relationship/user-list-row.css +++ b/web/settings/relationship/user-list-row.css @@ -1,39 +1,31 @@ .container { display: flex; justify-content: space-between; padding: 16px; color: var(--relationship-modal-color); font-size: var(--l-font-18); line-height: var(--line-height-display); } .usernameContainer { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .buttons { display: flex; flex-direction: row; align-items: center; gap: 8px; } .button { font-size: var(--s-font-14); line-height: var(--line-height-text); color: var(--btn-bg-filled); - cursor: pointer; - background: none; - border: none; - white-space: nowrap; -} - -.destructive { - color: var(--btn-bg-danger); } .edit_menu { position: relative; }