diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx index 3b724bbc8..b94e1dd98 100644 --- a/ts/components/MemberListItem.tsx +++ b/ts/components/MemberListItem.tsx @@ -378,7 +378,6 @@ export const MemberListItem = ({ active={isSelected} value={pubkey} inputName={pubkey} - label="" inputDataTestId="select-contact" /> diff --git a/ts/components/SessionSearchInput.tsx b/ts/components/SessionSearchInput.tsx index 6fcbbeeb8..af6d1e195 100644 --- a/ts/components/SessionSearchInput.tsx +++ b/ts/components/SessionSearchInput.tsx @@ -14,7 +14,7 @@ import { LucideIcon } from './icon/LucideIcon'; const StyledSearchInput = styled.div` height: var(--search-input-height); - background-color: var(--background-secondary-color); + background-color: var(--background-tertiary-color); width: 100%; // max width because it doesn't look good on a wide dialog otherwise max-width: 300px; @@ -23,8 +23,8 @@ const StyledSearchInput = styled.div` display: inline-flex; align-items: center; flex-shrink: 0; - border-radius: 100px; padding-inline: var(--margins-sm); + border-radius: 100px; `; const StyledInput = styled.input` @@ -88,8 +88,15 @@ export const SessionSearchInput = ({ searchType }: { searchType: SearchType }) = ? localize('searchContacts').toString() : localize('search').toString(); + const isInMainScreen = searchType === 'global' || searchType === 'create-group'; + + const backgroundColor = isInMainScreen ? 'transparent' : undefined; + return ( - + ` +const StyledContainer = styled.div<{ disabled: boolean }>` cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; min-height: 30px; background-color: var(--transparent-color); `; -const StyledInput = styled.input<{ - filledSize: number; - outlineOffset: number; - selectedColor?: string; +const StyledRadioOuter = styled.div<{ $disabled: boolean; $diameterRadioBorder: number }>` + width: ${props => props.$diameterRadioBorder}px; + height: ${props => props.$diameterRadioBorder}px; + border: 1px solid var(--text-primary-color); + border-radius: 50%; + background-color: transparent; + cursor: ${props => (props.$disabled ? 'not-allowed' : 'pointer')}; + position: relative; +`; + +const StyledSelectedInner = styled.div<{ + $disabled: boolean; + $selected: boolean; + $diameterRadioBg: number; }>` - opacity: 0; + width: ${props => props.$diameterRadioBg}px; + height: ${props => props.$diameterRadioBg}px; + border-radius: 50%; + opacity: ${props => (props.$selected ? 1 : 0)}; + background: ${props => (props.$disabled ? 'var(--disabled-color)' : 'var(--primary-color)')}; + pointer-events: none; + transition-duration: var(--default-duration); position: absolute; - width: ${props => props.filledSize + props.outlineOffset}px; - height: ${props => props.filledSize + props.outlineOffset}px; - - &:checked + label:before { - background: ${props => - props.disabled - ? 'var(--disabled-color)' - : props.selectedColor - ? props.selectedColor - : 'var(--primary-color)'}; - } + top: 50%; + left: 50%; + transform: translate(-50%, -50%); `; -// NOTE (Will): We don't use a transition because it's too slow and creates flickering when changing buttons. -const StyledLabel = styled.label<{ +function RadioButton({ + disabled, + onClick, + selected, + dataTestId, + diameterRadioBg, + diameterRadioBorder, + style, + ariaLabel, +}: { + selected: boolean; + onClick: MouseEventHandler; disabled: boolean; - filledSize: number; - outlineOffset: number; - beforeMargins?: string; + dataTestId: SessionDataTestId | undefined; + diameterRadioBg: number; + diameterRadioBorder: number; + style?: CSSProperties; + ariaLabel?: string; +}) { + // clickHandler is on the parent button, so we need to skip this input while pressing Tab + return ( + + + + ); +} + +// NOTE (): We don't use a transition because it's too slow and creates flickering when changing buttons. +const StyledLabel = styled.label<{ + $disabled: boolean; }>` cursor: pointer; - color: ${props => (props.disabled ? 'var(--disabled-color)' : 'var(--text-primary-color)')}; - - &:before { - content: ''; - display: inline-block; - border-radius: 100%; - - padding: ${props => props.filledSize}px; - border: none; - outline: 1px solid currentColor; /* CSS variables don't work here */ - outline-offset: ${props => props.outlineOffset}px; - ${props => props.beforeMargins && `margin: ${props.beforeMargins};`}; - } + color: ${props => (props.$disabled ? 'var(--disabled-color)' : 'var(--text-primary-color)')}; + margin-inline-end: var(--margins-sm); `; type SessionRadioProps = { - label: string; + label?: string; value: string; active: boolean; inputName?: string; - beforeMargins?: string; onClick?: (value: string) => void; disabled?: boolean; - radioPosition?: 'left' | 'right'; style?: CSSProperties; labelDataTestId?: SessionDataTestId; inputDataTestId?: SessionDataTestId; @@ -69,19 +100,16 @@ type SessionRadioProps = { export const SessionRadio = (props: SessionRadioProps) => { const { label, - inputName, value, active, onClick, - beforeMargins, disabled = false, - radioPosition = 'left', style, labelDataTestId, inputDataTestId, } = props; - const clickHandler = (e: SyntheticEvent) => { + const clickHandler = (e: React.MouseEvent | React.KeyboardEvent) => { if (!disabled && onClick) { // let something else catch the event if our click handler is not set e.stopPropagation(); @@ -89,11 +117,11 @@ export const SessionRadio = (props: SessionRadioProps) => { } }; - const filledSize = 15 / 2; - const outlineOffset = 2; + const diameterRadioBorder = 26; + const diameterRadioBg = 20; return ( - { if (e.code === 'Space') { clickHandler(e); @@ -104,112 +132,77 @@ export const SessionRadio = (props: SessionRadioProps) => { > - - + {label} + + ) : null} + + - {label} - + dataTestId={inputDataTestId} + diameterRadioBorder={diameterRadioBorder} + diameterRadioBg={diameterRadioBg} + /> - + ); }; -const StyledInputOutlineSelected = styled(StyledInput)` - color: ${props => (props.disabled ? 'var(--disabled-color)' : 'var(--text-primary-color)')}; - - label:before, - label:before { - outline: none; - } - - &:checked + label:before { - outline: 1px solid currentColor; - } -`; -const StyledLabelOutlineSelected = styled(StyledLabel)<{ selectedColor: string }>` - &:before { - background: ${props => - props.disabled - ? 'var(--disabled-color)' - : props.selectedColor - ? props.selectedColor - : 'var(--primary-color)'}; - outline: 1px solid transparent; /* CSS variables don't work here */ - } -`; - /** - * Keeping this component here so we can reuse the `StyledInput` and `StyledLabel` defined locally rather than exporting them + * This is slightly different that the classic SessionRadio as this one has + * - no padding between the selected background and the border, + * - they all have a background color (even when not selected), but the border is present on the selected one + * + * Keeping it here so we don't have to export */ export const SessionRadioPrimaryColors = (props: { value: string; active: boolean; - inputName?: string; onClick: (value: string) => void; - ariaLabel?: string; + ariaLabel: string; color: string; // by default, we use the theme accent color but for the settings screen we need to be able to force it - disabled?: boolean; }) => { - const { inputName, value, active, onClick, color, ariaLabel, disabled = false } = props; + const { value, active, onClick, color, ariaLabel } = props; function clickHandler(e: ChangeEvent) { e.stopPropagation(); onClick(value); } - const filledSize = 31 / 2; - const outlineOffset = 5; + // this component has no padding between the selected background and the border + const diameterRadioBorder = 26; + const diameterRadioBg = 22; + + const overriddenColorsVars = { + '--primary-color': color, + '--text-primary-color': active ? undefined : 'transparent', + } as React.CSSProperties; return ( - - - - {''} - + disabled={false} + dataTestId={undefined} + diameterRadioBorder={diameterRadioBorder} + diameterRadioBg={diameterRadioBg} + style={overriddenColorsVars} + ariaLabel={ariaLabel} + /> ); }; diff --git a/ts/components/basic/SessionRadioGroup.tsx b/ts/components/basic/SessionRadioGroup.tsx index 0c0180423..e1c1fbd9f 100644 --- a/ts/components/basic/SessionRadioGroup.tsx +++ b/ts/components/basic/SessionRadioGroup.tsx @@ -17,7 +17,6 @@ interface Props { items: SessionRadioItems; group: string; onClick: (selectedValue: string) => void; - radioPosition?: 'left' | 'right'; style?: CSSProperties; } @@ -29,16 +28,15 @@ const StyledFieldSet = styled.fieldset` margin-inline-start: var(--margins-sm); margin-top: var(--margins-sm); + min-width: 300px; // so it doesn't look too weird on the modal (which is 410px wide) + & > div { padding: var(--margins-md) 7px; } - & > div + div { - border-top: 1px solid var(--border-color); - } `; export const SessionRadioGroup = (props: Props) => { - const { items, group, initialItem, radioPosition, style } = props; + const { items, group, initialItem, style } = props; const [activeItem, setActiveItem] = useState(''); useMount(() => { @@ -63,8 +61,6 @@ export const SessionRadioGroup = (props: Props) => { setActiveItem(value); props.onClick(value); }} - beforeMargins={'0 var(--margins-sm) 0 0 '} - radioPosition={radioPosition} style={{ textAlign: 'start' }} /> ); diff --git a/ts/components/buttons/PanelRadioButton.tsx b/ts/components/buttons/PanelRadioButton.tsx index 2c792b3da..902aedace 100644 --- a/ts/components/buttons/PanelRadioButton.tsx +++ b/ts/components/buttons/PanelRadioButton.tsx @@ -48,7 +48,6 @@ export const PanelRadioButton = (props: PanelRadioButtonProps) => { active={isSelected} value={value} inputName={value} - label="" disabled={disabled} inputDataTestId={radioInputDataTestId} style={{ paddingInlineEnd: 'var(--margins-xs)' }} diff --git a/ts/components/conversation/header/ConversationHeaderItems.tsx b/ts/components/conversation/header/ConversationHeaderItems.tsx index 06f078345..958322d17 100644 --- a/ts/components/conversation/header/ConversationHeaderItems.tsx +++ b/ts/components/conversation/header/ConversationHeaderItems.tsx @@ -11,7 +11,6 @@ import { useSelectedIsPrivate, useSelectedIsPrivateFriend, useSelectedIsPublic, - useSelectedWeAreAdmin, } from '../../../state/selectors/selectedConversation'; import { Avatar, AvatarSize } from '../../avatar/Avatar'; import { SessionIconButton } from '../../icon'; @@ -28,12 +27,11 @@ export const AvatarHeader = (props: { pubkey: string; onAvatarClick?: () => void const isGroupV2 = useIsGroupV2(pubkey); const isPublic = useSelectedIsPublic(); - const weAreAdmin = useSelectedWeAreAdmin(); const canClickLegacy = isLegacyGroup && false; // we can never click the avatar if it's a legacy group const canClickPrivateApproved = isApproved && isPrivate; // we can only click the avatar if it's a private and approved conversation const canClick03GroupAccepted = isGroupV2 && !invitePending; // we can only click the avatar if it's a group and have accepted the invite already - const canClickCommunity = isPublic && weAreAdmin; + const canClickCommunity = isPublic; // we can always click the settings for a community (even if we are not an admin) const optOnAvatarClick = canClickLegacy || canClickPrivateApproved || canClick03GroupAccepted || canClickCommunity diff --git a/ts/components/dialog/DeleteAccountModal.tsx b/ts/components/dialog/DeleteAccountModal.tsx index a25577c95..34d026efc 100644 --- a/ts/components/dialog/DeleteAccountModal.tsx +++ b/ts/components/dialog/DeleteAccountModal.tsx @@ -41,7 +41,11 @@ const DescriptionBeforeAskingConfirmation = (props: { return ( <> - + @@ -158,7 +162,7 @@ export const DeleteAccountModal = () => { dataTestId="session-confirm-cancel-button" /> - + {isLoading && } diff --git a/ts/components/dialog/SessionConfirm.tsx b/ts/components/dialog/SessionConfirm.tsx index 6a1e9bac8..5944f70ce 100644 --- a/ts/components/dialog/SessionConfirm.tsx +++ b/ts/components/dialog/SessionConfirm.tsx @@ -219,7 +219,6 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => { group="session-confirm-radio-group" initialItem={chosenOption} items={radioOptions} - radioPosition="right" onClick={value => { if (value) { setChosenOption(value); diff --git a/ts/components/dialog/UpdateGroupNameDialog.tsx b/ts/components/dialog/UpdateGroupNameDialog.tsx index 563b34c7c..0095654e0 100644 --- a/ts/components/dialog/UpdateGroupNameDialog.tsx +++ b/ts/components/dialog/UpdateGroupNameDialog.tsx @@ -209,7 +209,7 @@ export function UpdateGroupNameDialog(props: { conversationId: string }) { text={localize('save').toString()} onClick={onClickOK} buttonType={SessionButtonType.Simple} - disabled={isNameChangePending || !newGroupName} + disabled={isNameChangePending || !newGroupName || !newGroupName.trim()} /> ; @@ -60,7 +58,6 @@ export const BlockOrUnblockDialog = ({ pubkeys, action, onConfirmed }: NonNullab action === 'block' ? localize('block').toString() : localize('blockUnblock').toString(); const args = useBlockUnblockI18nDescriptionArgs({ action, pubkeys }); - const selectedConversation = useSelectedConversationKey(); const closeModal = useCallback(() => { dispatch(updateBlockOrUnblockModal(null)); @@ -69,7 +66,6 @@ export const BlockOrUnblockDialog = ({ pubkeys, action, onConfirmed }: NonNullab const [, onConfirm] = useAsyncFn(async () => { if (action === 'block') { - const firstPubkeyBlocked = pubkeys?.[0] || undefined; // we never block more than one user from the UI, so this is not very useful, just a type guard for (let index = 0; index < pubkeys.length; index++) { const pubkey = pubkeys[index]; @@ -77,11 +73,8 @@ export const BlockOrUnblockDialog = ({ pubkeys, action, onConfirmed }: NonNullab // eslint-disable-next-line no-await-in-loop await BlockedNumberController.block(pubkey); } - // Note we don't want to close the CS modal if it was shown, now. - // reset the selected convo if it was the one we blocked - if (firstPubkeyBlocked && selectedConversation === firstPubkeyBlocked) { - dispatch(resetConversationExternal()); - } + // Note: we don't want to close the CS modal if it was shown, now. + // Nor reset the conversation if it was shown. } else { await BlockedNumberController.unblockAll(pubkeys); } diff --git a/ts/components/dialog/conversationSettings/pages/default/defaultPage.tsx b/ts/components/dialog/conversationSettings/pages/default/defaultPage.tsx index 3aedba4eb..858f7f870 100644 --- a/ts/components/dialog/conversationSettings/pages/default/defaultPage.tsx +++ b/ts/components/dialog/conversationSettings/pages/default/defaultPage.tsx @@ -96,7 +96,6 @@ function DestructiveActions({ conversationId }: WithConvoId) { - @@ -126,6 +125,7 @@ function DefaultPageForPrivate({ conversationId }: WithConvoId) { + {/* Below are "destructive" actions */} diff --git a/ts/components/settings/SettingsThemeSwitcher.tsx b/ts/components/settings/SettingsThemeSwitcher.tsx index bf819925c..d1834cbc2 100644 --- a/ts/components/settings/SettingsThemeSwitcher.tsx +++ b/ts/components/settings/SettingsThemeSwitcher.tsx @@ -96,7 +96,6 @@ const Themes = () => { {theme.title} { key={item.id} active={item.id === selectedPrimaryColor} value={item.id} - inputName="primary-colors" ariaLabel={item.ariaLabel} color={item.color} onClick={() => { diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index c0d578c03..f9039dfbf 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -386,7 +386,11 @@ export async function innerHandleSwarmContentMessage({ window.log.info('innerHandleSwarmContentMessage'); const content = SignalService.Content.decode(new Uint8Array(contentDecrypted)); - if (!shouldProcessContentMessage(envelope, content, false)) { + // This function gets called with an inbox content from a community. When that's the case, + // the messageHash is empty. + // `shouldProcessContentMessage` is a lot less strict in terms of timestamps for community messages, and needs to be. + // Not having this isCommunity flag set to true would make any incoming message from a blinded message request be dropped. + if (!shouldProcessContentMessage(envelope, content, !messageHash)) { window.log.info( `innerHandleSwarmContentMessage: dropping invalid content message ${envelope.timestamp}` ); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 0a8a780c6..39500d2b9 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -31,6 +31,7 @@ import { Storage } from '../../util/storage'; import { SettingsKey } from '../../data/settings-key'; import { sectionActions } from './section'; import { ed25519Str } from '../../session/utils/String'; +import { UserUtils } from '../../session/utils'; export type MessageModelPropsWithoutConvoProps = { propsForMessage: PropsForMessageWithoutConvoProps; @@ -1038,13 +1039,14 @@ function applyConversationsChanged( } if ( - state.selectedConversation && + selectedConversation && convoProps.isPrivate && convoProps.id === selectedConversation && convoProps.priority && - convoProps.priority < CONVERSATION_PRIORITIES.default + convoProps.priority < CONVERSATION_PRIORITIES.default && + selectedConversation !== UserUtils.getOurPubKeyStrFromCache() ) { - // A private conversation hidden cannot be a selected. + // A private conversation hidden cannot be selected (except the Note To Self) // When opening a hidden conversation, we unhide it so it can be selected again. state.selectedConversation = undefined; }