diff --git a/ts/components/AboutView.tsx b/ts/components/AboutView.tsx index 05f97a140..751e460a1 100644 --- a/ts/components/AboutView.tsx +++ b/ts/components/AboutView.tsx @@ -21,8 +21,12 @@ const StyledContent = styled(Flex)` color: var(--text-primary-color); } - img { - margin: var(--margins-lg) 0 var(--margins-md); + img:first-child { + margin: var(--margins-2xl) 0 var(--margins-lg); + } + + img:nth-child(2) { + margin-bottom: var(--margins-xl); } .session-button { @@ -67,7 +71,18 @@ export const AboutView = () => { justifyContent={'center'} alignItems={'center'} > - session icon + session brand icon + session brand text ` cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; display: flex; @@ -40,11 +41,16 @@ const StyledSessionMemberItem = styled.button<{ ? 'var(--conversation-tab-background-selected-color) !important' : null}; - &:not(button:last-child) { + ${props => props.inMentions && 'max-width: 300px;'} + ${props => + props.withBorder && + `&:not(button:last-child) { border-bottom: 1px solid var(--border-color); - } + }`} - ${props => props.inMentions && 'max-width: 300px;'} + &:hover { + background-color: var(--conversation-tab-background-hover-color); + } `; const StyledInfo = styled.div` @@ -75,6 +81,7 @@ export const MemberListItem = (props: { isZombie?: boolean; inMentions?: boolean; // set to true if we are rendering members but in the Mentions picker disableBg?: boolean; + withBorder?: boolean; maxNameWidth?: string; isAdmin?: boolean; // if true, we add a small crown on top of their avatar onSelect?: (pubkey: string) => void; @@ -91,6 +98,7 @@ export const MemberListItem = (props: { onUnselect, inMentions, disableBg, + withBorder = true, maxNameWidth, disabled, dataTestId, @@ -104,18 +112,12 @@ export const MemberListItem = (props: { // eslint-disable-next-line no-unused-expressions isSelected ? onUnselect?.(pubkey) : onSelect?.(pubkey); }} - style={ - !inMentions && !disableBg - ? { - backgroundColor: 'var(--background-primary-color)', - } - : {} - } data-testid={dataTestId} zombie={isZombie} inMentions={inMentions} selected={isSelected} disableBg={disableBg} + withBorder={withBorder} disabled={disabled} > diff --git a/ts/components/SessionSearchInput.tsx b/ts/components/SessionSearchInput.tsx index d8df8fc7b..bfe7e5a2b 100644 --- a/ts/components/SessionSearchInput.tsx +++ b/ts/components/SessionSearchInput.tsx @@ -1,12 +1,13 @@ import { Dispatch } from '@reduxjs/toolkit'; import { debounce } from 'lodash'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { clearSearch, search, updateSearchTerm } from '../state/ducks/search'; import { getConversationsCount } from '../state/selectors/conversations'; import { getLeftOverlayMode } from '../state/selectors/section'; import { SessionIconButton } from './icon'; +import { useHotkey } from '../hooks/useHotkey'; const StyledSearchInput = styled.div` height: var(--search-input-height); @@ -70,6 +71,15 @@ export const SessionSearchInput = () => { const isGroupCreationSearch = useSelector(getLeftOverlayMode) === 'closed-group'; const convoCount = useSelector(getConversationsCount); + const inputRef = useRef(null); + + useHotkey('Escape', () => { + if (inputRef.current !== null && inputRef.current === document.activeElement) { + setCurrentSearchTerm(''); + dispatch(clearSearch()); + } + }); + // just after onboard we only have a conversation with ourself if (convoCount <= 1) { return null; @@ -87,6 +97,7 @@ export const SessionSearchInput = () => { iconType="search" /> { const inputValue = e.target.value; diff --git a/ts/components/conversation/SubtleNotification.tsx b/ts/components/conversation/SubtleNotification.tsx index 53813f592..86bbc8422 100644 --- a/ts/components/conversation/SubtleNotification.tsx +++ b/ts/components/conversation/SubtleNotification.tsx @@ -10,10 +10,12 @@ import { useSelectedConversationKey, useSelectedHasDisabledBlindedMsgRequests, useSelectedIsNoteToSelf, + useSelectedIsPrivate, useSelectedNicknameOrProfileNameOrShortenedPubkey, } from '../../state/selectors/selectedConversation'; import { LocalizerKeys } from '../../types/LocalizerKeys'; import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer'; +import { SessionUtilContact } from '../../session/utils/libsession/libsession_utils_contacts'; const Container = styled.div` display: flex; @@ -40,15 +42,24 @@ export const ConversationOutgoingRequestExplanation = () => { const showMsgRequestUI = selectedConversation && isOutgoingMessageRequest; - if (!showMsgRequestUI || hasIncomingMessages) { + const selectedIsPrivate = useSelectedIsPrivate(); + + if (!showMsgRequestUI || hasIncomingMessages || !selectedIsPrivate) { return null; } - - return ( - - {window.i18n('messageRequestPendingDescription')} - - ); + const contactFromLibsession = SessionUtilContact.getContactCached(selectedConversation); + // Note: we want to display this description when the conversation is private (or blinded) AND + // - the conversation is brand new (and not saved yet in libsession: transient conversation), + // - the conversation exists in libsession but we are not approved yet. + // This works because a blinded conversation is not saved in libsession currently, and will only be once approved_me is true + if (!contactFromLibsession || !contactFromLibsession.approvedMe) { + return ( + + {window.i18n('messageRequestPendingDescription')} + + ); + } + return null; }; /** @@ -79,7 +90,7 @@ export const ConversationIncomingRequestExplanation = () => { export const NoMessageInConversation = () => { const selectedConversation = useSelectedConversationKey(); - const hasMessage = useSelector(getSelectedHasMessages); + const hasMessages = useSelector(getSelectedHasMessages); const isMe = useSelectedIsNoteToSelf(); const canWrite = useSelector(getSelectedCanWrite); @@ -87,7 +98,7 @@ export const NoMessageInConversation = () => { // TODOLATER use this selector across the whole application (left pane excluded) const nameToRender = useSelectedNicknameOrProfileNameOrShortenedPubkey(); - if (!selectedConversation || hasMessage) { + if (!selectedConversation || hasMessages) { return null; } let localizedKey: LocalizerKeys = 'noMessagesInEverythingElse'; diff --git a/ts/components/dialog/UserDetailsDialog.tsx b/ts/components/dialog/UserDetailsDialog.tsx index 36f447c91..baa388e20 100644 --- a/ts/components/dialog/UserDetailsDialog.tsx +++ b/ts/components/dialog/UserDetailsDialog.tsx @@ -16,8 +16,9 @@ import { ConversationTypeEnum } from '../../models/types'; const StyledInputContainer = styled(Flex)` textarea { + cursor: default; overflow: hidden; - padding-top: var(--margins-xs); + top: 14px; } `; diff --git a/ts/components/dialog/edit-profile/EditProfileDialog.tsx b/ts/components/dialog/edit-profile/EditProfileDialog.tsx index b4ffdf7d9..7657c0bb1 100644 --- a/ts/components/dialog/edit-profile/EditProfileDialog.tsx +++ b/ts/components/dialog/edit-profile/EditProfileDialog.tsx @@ -216,6 +216,7 @@ export const EditProfileDialog = () => { setLoading(true); const validName = await ProfileManager.updateOurProfileDisplayName(profileName); setUpdateProfileName(validName); + setProfileName(validName); setMode('default'); } catch (err) { window.log.error('Profile update error', err); @@ -351,7 +352,7 @@ export const EditProfileDialog = () => { {!loading ? : null} - {mode === 'default' || mode === 'qr' ? ( + {mode === 'default' || mode === 'qr' || mode === 'lightbox' ? ( { const lightBoxOptions = prepareQRCodeForLightBox(fileName, dataUrl, () => { - setMode('edit'); + setMode('qr'); }); window.inboxStore?.dispatch(updateLightBoxOptions(lightBoxOptions)); setMode('lightbox'); diff --git a/ts/components/inputs/SessionInput.tsx b/ts/components/inputs/SessionInput.tsx index f52ec154f..2f30ce0e3 100644 --- a/ts/components/inputs/SessionInput.tsx +++ b/ts/components/inputs/SessionInput.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, ReactNode, RefObject, useEffect, useState } from 'react'; +import { ChangeEvent, ReactNode, RefObject, useEffect, useRef, useState } from 'react'; import { motion } from 'framer-motion'; import { isEmpty, isEqual } from 'lodash'; @@ -13,7 +13,7 @@ type TextSizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; const StyledSessionInput = styled(Flex)<{ error: boolean; - textSize?: TextSizes; + textSize: TextSizes; }>` position: relative; width: 100%; @@ -68,6 +68,7 @@ const StyledBorder = styled(AnimatedFlex)` const StyledInput = styled(motion.input)<{ error: boolean; + textSize: TextSizes; centerText?: boolean; monospaced?: boolean; }>` @@ -79,9 +80,9 @@ const StyledInput = styled(motion.input)<{ color: ${props => (props.error ? 'var(--danger-color)' : 'var(--input-text-color)')}; font-family: ${props => (props.monospaced ? 'var(--font-mono)' : 'var(--font-default)')}; - font-size: 12px; line-height: 1.4; ${props => props.centerText && 'text-align: center;'} + ${props => `font-size: var(--font-size-${props.textSize});`} &::placeholder { color: var(--input-text-placeholder-color); @@ -89,13 +90,15 @@ const StyledInput = styled(motion.input)<{ } `; -const StyledTextAreaContainer = styled(motion.div)<{ +export const StyledTextAreaContainer = styled(motion.div)<{ noValue: boolean; error: boolean; + textSize: TextSizes; centerText?: boolean; - textSize?: TextSizes; monospaced?: boolean; }>` + display: flex; + align-items: center; overflow: hidden; position: relative; height: ${props => (props.textSize ? `calc(var(--font-size-${props.textSize}) * 4)` : '48px')}; @@ -107,8 +110,8 @@ const StyledTextAreaContainer = styled(motion.div)<{ outline: 0; font-family: ${props => (props.monospaced ? 'var(--font-mono)' : 'var(--font-default)')}; - font-size: 12px; - line-height: 1.4; + ${props => `font-size: var(--font-size-${props.textSize});`} + line-height: 1; ${props => props.centerText && 'text-align: center;'} @@ -121,21 +124,20 @@ const StyledTextAreaContainer = styled(motion.div)<{ border: none; background: transparent; - ${props => - props.noValue && - `position: absolute; - top: ${props.textSize ? `calc(var(--font-size-${props.textSize}) + 5px)` : 'calc(12px + 5px)'};`} + position: absolute; + top: ${props => + `calc(var(--font-size-${props.textSize}) + ${props.textSize === 'xl' ? '8px' : '5px'})`}; resize: none; - overflow-wrap: break-word; + word-break: break-all; user-select: all; ${props => props.centerText && 'text-align: center;'} &:placeholder-shown { font-family: ${props => (props.monospaced ? 'var(--font-mono)' : 'var(--font-default)')}; - font-size: 12px; - line-height: 1.4; + ${props => `font-size: var(--font-size-${props.textSize});`} + line-height: 1; } &::placeholder { @@ -267,7 +269,7 @@ export const SessionInput = (props: Props) => { showHideButtonDataTestIds, ctaButton, monospaced, - textSize, + textSize = 'sm', centerText, editable = true, isTextArea, @@ -277,8 +279,11 @@ export const SessionInput = (props: Props) => { } = props; const [inputValue, setInputValue] = useState(''); const [errorString, setErrorString] = useState(''); + const [textErrorStyle, setTextErrorStyle] = useState(false); const [forceShow, setForceShow] = useState(false); + const textAreaRef = useRef(inputRef?.current || null); + const correctType = forceShow ? 'text' : type; const updateInputValue = (e: ChangeEvent) => { @@ -288,6 +293,17 @@ export const SessionInput = (props: Props) => { e.preventDefault(); const val = e.target.value; setInputValue(val); + setTextErrorStyle(false); + if (isTextArea && textAreaRef && textAreaRef.current !== null) { + const scrollHeight = `${textAreaRef.current.scrollHeight}px`; + if (isEmpty(val)) { + // resets the height of the text area so it's centered if we clear the text + textAreaRef.current.style.height = 'unset'; + } + if (scrollHeight !== textAreaRef.current.style.height) { + textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`; + } + } if (onValueChanged) { onValueChanged(val); } @@ -299,6 +315,7 @@ export const SessionInput = (props: Props) => { type: correctType, placeholder, value, + textSize, disabled: !editable, maxLength, autoFocus, @@ -306,7 +323,6 @@ export const SessionInput = (props: Props) => { required, 'aria-required': required, tabIndex, - ref: inputRef, onChange: updateInputValue, style: { paddingInlineEnd: enableShowHideButton ? '48px' : undefined }, // just in case onChange isn't triggered @@ -332,7 +348,7 @@ export const SessionInput = (props: Props) => { const containerProps = { noValue: isEmpty(value), - error: Boolean(error), + error: textErrorStyle, centerText, textSize, monospaced, @@ -342,6 +358,7 @@ export const SessionInput = (props: Props) => { useEffect(() => { if (error && !isEmpty(error) && !isEqual(error, errorString)) { setErrorString(error); + setTextErrorStyle(!!error); } }, [error, errorString]); @@ -369,12 +386,17 @@ export const SessionInput = (props: Props) => { > {isTextArea ? ( -