Merge remote-tracking branch 'upstream/unstable' into closed-group-chunk3

pull/3052/head
Audric Ackermann 11 months ago
commit 6e137fe69f

@ -22,14 +22,15 @@ Build instructions can be found in [Contributing.md](CONTRIBUTING.md).
## Verifying signatures
Get Kee's key and import it:
```
wget https://raw.githubusercontent.com/oxen-io/oxen-core/dev/utils/gpg_keys/KeeJef.asc
gpg --import KeeJef.asc
```
Get the signed hash for this release, the SESSION_VERSION needs to be updated for the release you want to verify
```
export SESSION_VERSION=1.6.1
wget https://github.com/oxen-io/session-desktop/releases/download/v$SESSION_VERSION/signatures.asc
@ -46,12 +47,12 @@ If it does, the hashes are valid but we still have to make the sure the signed h
Make sure the two commands below returns the same hash.
If they do, files are valid
```
sha256sum session-desktop-linux-amd64-$SESSION_VERSION.deb
grep .deb signatures.asc
```
## Debian repository
Please visit https://deb.oxen.io/<br/>
@ -62,3 +63,7 @@ Copyright 2011 Whisper Systems<br/>
Copyright 2013-2017 Open Whisper Systems<br/>
Copyright 2019-2023 The Oxen Project<br/>
Licensed under the GPLv3: https://www.gnu.org/licenses/gpl-3.0.html<br/>
## Attributions
The IP-to-country mapping data used in this project is provided by [MaxMind GeoLite2](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data).

@ -617,5 +617,6 @@
"resolution": "Resolution",
"duration": "Duration",
"notApplicable": "N/A",
"unknownError": "Unknown Error"
"unknownError": "Unknown Error",
"displayNameErrorNew": "We were unable to load your display name. Please enter a new display name to continue."
}

@ -5,6 +5,8 @@
<!-- Mac distribution -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>

@ -5,6 +5,10 @@
<!-- Mac app store -->
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>

@ -29,7 +29,7 @@ exports.default = async function notarizing(context) {
}
const options = {
appBundleId: 'org.getsession.desktop',
appBundleId: 'com.loki-project.messenger-desktop',
appPath: `${appOutDir}/${appName}.app`,
appleId: SIGNING_APPLE_ID,
appleIdPassword: SIGNING_APP_PASSWORD,

Binary file not shown.

@ -2,7 +2,7 @@
"name": "session-desktop",
"productName": "Session",
"description": "Private messaging from your desktop",
"version": "1.12.0",
"version": "1.12.3",
"license": "GPL-3.0",
"author": {
"name": "Oxen Labs",
@ -78,13 +78,12 @@
"auto-bind": "^4.0.0",
"backbone": "1.3.3",
"blob-util": "2.0.2",
"blueimp-load-image": "5.14.0",
"blueimp-load-image": "^5.16.0",
"buffer-crc32": "0.2.13",
"bunyan": "https://github.com/Bilb/node-bunyan",
"bytebuffer": "^5.0.1",
"classnames": "2.2.5",
"config": "1.28.1",
"country-code-lookup": "^0.0.19",
"curve25519-js": "https://github.com/oxen-io/curve25519-js",
"date-fns": "^3.3.1",
"dompurify": "^2.0.7",
@ -93,15 +92,16 @@
"emoji-mart": "^5.5.2",
"filesize": "3.6.1",
"firstline": "1.2.1",
"focus-trap-react": "^10.2.3",
"fs-extra": "9.0.0",
"glob": "10.3.10",
"image-type": "^4.1.0",
"ip2country": "1.0.1",
"libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.4.3/libsession_util_nodejs-v0.4.3.tar.gz",
"libsodium-wrappers-sumo": "^0.7.9",
"linkify-it": "^4.0.1",
"lodash": "^4.17.21",
"long": "^4.0.0",
"maxmind": "^4.3.18",
"mic-recorder-to-mp3": "^2.2.2",
"moment": "^2.29.4",
"node-fetch": "^2.6.7",
@ -140,7 +140,7 @@
"@commitlint/types": "^17.4.4",
"@electron/notarize": "^2.1.0",
"@types/backbone": "1.4.2",
"@types/blueimp-load-image": "5.14.4",
"@types/blueimp-load-image": "^5.16.2",
"@types/buffer-crc32": "^0.2.0",
"@types/bunyan": "^1.8.8",
"@types/bytebuffer": "^5.0.41",
@ -166,7 +166,7 @@
"@types/rimraf": "2.0.2",
"@types/semver": "5.5.0",
"@types/sinon": "9.0.4",
"@types/styled-components": "^5.1.4",
"@types/styled-components": "5.1.1",
"@types/uuid": "8.3.4",
"@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "7.1.0",
@ -176,7 +176,7 @@
"cross-env": "^6.0.3",
"css-loader": "^6.7.2",
"dmg-builder": "23.6.0",
"electron": "^25.8.4",
"electron": "25.8.4",
"electron-builder": "23.0.8",
"eslint": "8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
@ -216,10 +216,13 @@
"afterSign": "build/notarize.js",
"afterPack": "build/afterPackHook.js",
"artifactName": "${name}-${os}-${arch}-${version}.${ext}",
"extraResources": {
"from": "./build/launcher-script.sh",
"to": "./launcher-script.sh"
},
"extraResources": [
{
"from": "./build/launcher-script.sh",
"to": "./launcher-script.sh"
},
"mmdb/GeoLite2-Country.mmdb"
],
"mac": {
"category": "public.app-category.social-networking",
"icon": "build/icon-mac.icns",

@ -29,7 +29,7 @@ window.getNodeVersion = () => configAny.node_version;
window.sessionFeatureFlags = {
useOnionRequests: true,
useTestNet: isTestNet(),
useTestNet: isTestNet() || isTestIntegration(),
useClosedGroupV2: true, // TODO DO NOT MERGE Remove after QA debugger
debug: {
debugLogging: !_.isEmpty(process.env.SESSION_DEBUG),

@ -188,7 +188,6 @@
display: flex;
align-items: center;
justify-content: center;
margin-right: -20px; // offsets the edit icon button so it's centered
p {
font-size: $session-font-md;

@ -57,6 +57,7 @@
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
padding: 10px;
border-radius: var(--border-radius-message-box);
@ -73,7 +74,6 @@
.module-message__generic-attachment__icon-container {
position: relative;
cursor: pointer;
}
.module-message__generic-attachment__spinner-container {
padding-inline-start: 4px;
@ -255,8 +255,6 @@
.module-conversation__user,
.module-message__author {
margin-top: var(--margins-sm);
margin-bottom: var(--margins-xs);
font-size: var(--font-size-sm);
font-weight: 300;
line-height: 18px;
@ -611,6 +609,9 @@
flex-direction: column;
align-items: stretch;
overflow: hidden;
max-height: 100%;
display: flex;
gap: 5px;
.session-icon-button:first-child {
margin-right: var(--margins-sm);
@ -621,6 +622,7 @@
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: var(--margins-xs);
}
.module-conversation-list-item__header__name {

@ -153,7 +153,6 @@ $session-margin-md: 15px;
$session-margin-lg: 20px;
// Animations
$session-transition-duration: 0.25s;
@keyframes fadein {
from {

@ -172,13 +172,13 @@
}
.rc-slider-tooltip-zoom-down-enter,
.rc-slider-tooltip-zoom-down-appear {
animation-duration: 0.3s;
animation-duration: var(--default-duration);
animation-fill-mode: both;
display: block !important;
animation-play-state: paused;
}
.rc-slider-tooltip-zoom-down-leave {
animation-duration: 0.3s;
animation-duration: var(--default-duration);
animation-fill-mode: both;
display: block !important;
animation-play-state: paused;

@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { switchThemeTo } from '../themes/switchTheme';
import { SessionTheme } from '../themes/SessionTheme';
import { fetch } from '../util/logging';
import { fetchNodeLog } from '../util/logging';
import { SessionButton, SessionButtonType } from './basic/SessionButton';
import { SessionIconButton } from './icon';
@ -78,7 +78,7 @@ const DebugLogViewAndSave = () => {
const commitHashInfo = window.getCommitHash() ? `Commit Hash: ${window.getCommitHash()}` : '';
// eslint-disable-next-line more/no-then
fetch()
fetchNodeLog()
.then((text: any) => {
const debugLogWithSystemInfo = `${operatingSystemInfo} ${commitHashInfo} ${text}`;
setContent(debugLogWithSystemInfo);

@ -0,0 +1,21 @@
import FocusTrap from 'focus-trap-react';
import React, { useState } from 'react';
import { useMount } from 'react-use';
/**
* Focus trap which activates on mount.
*/
export function SessionFocusTrap(props: { children: React.ReactNode }) {
const [active, setActive] = useState(false);
// Activate the trap on mount so we **should** have a button to tab through. focus-trap-react will throw if we don't have a button when the trap becomes active.
// We might still have an issue for dialogs which have buttons added with a useEffect, or asynchronously but have no buttons on mount.
// If that happens we will need to force those dialogs to have at least one button so focus-trap-react does not throw.
useMount(() => setActive(true));
return (
<FocusTrap active={active} focusTrapOptions={{ initialFocus: false, allowOutsideClick: true }}>
{props.children}
</FocusTrap>
);
}

@ -100,8 +100,20 @@ async function createSessionInboxStore() {
async function setupLeftPane(forceUpdateInboxComponent: () => void) {
window.openConversationWithMessages = openConversationWithMessages;
window.inboxStore = await createSessionInboxStore();
window.inboxStore.dispatch(updateAllOnStorageReady());
window.inboxStore.dispatch(
updateAllOnStorageReady({
hasBlindedMsgRequestsEnabled: Storage.getBoolOrFalse(
SettingsKey.hasBlindedMsgRequestsEnabled
),
someDeviceOutdatedSyncing: Storage.getBoolOrFalse(SettingsKey.someDeviceOutdatedSyncing),
settingsLinkPreview: Storage.getBoolOrFalse(SettingsKey.settingsLinkPreview),
hasFollowSystemThemeEnabled: Storage.getBoolOrFalse(SettingsKey.hasFollowSystemThemeEnabled),
hasShiftSendEnabled: Storage.getBoolOrFalse(SettingsKey.hasShiftSendEnabled),
})
);
window.inboxStore.dispatch(groupInfoActions.loadMetaDumpsFromDB()); // this loads the dumps from DB and fills the 03-groups slice with the corresponding details
forceUpdateInboxComponent();
window.getState = window.inboxStore.getState;
@ -122,7 +134,7 @@ const SomeDeviceOutdatedSyncingNotice = () => {
text={window.i18n('someOfYourDeviceUseOutdatedVersion')}
onButtonClick={dismiss}
icon="exit"
dataTestId='some-of-your-devices-outdated-inbox'
dataTestId="some-of-your-devices-outdated-inbox"
/>
);
};

@ -3,8 +3,9 @@ import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { getShowScrollButton } from '../state/selectors/conversations';
import { useSelectedUnreadCount } from '../state/selectors/selectedConversation';
import { SessionIconButton } from './icon';
import { Noop } from '../types/Util';
import { SessionUnreadCount } from './icon/SessionNotificationCount';
const SessionScrollButtonDiv = styled.div`
position: fixed;
@ -18,8 +19,9 @@ const SessionScrollButtonDiv = styled.div`
}
`;
export const SessionScrollButton = (props: { onClickScrollBottom: Noop }) => {
export const SessionScrollButton = (props: { onClickScrollBottom: () => void }) => {
const show = useSelector(getShowScrollButton);
const unreadCount = useSelectedUnreadCount();
return (
<SessionScrollButtonDiv>
@ -29,7 +31,9 @@ export const SessionScrollButton = (props: { onClickScrollBottom: Noop }) => {
isHidden={!show}
onClick={props.onClickScrollBottom}
dataTestId="scroll-to-bottom-button"
/>
>
{Boolean(unreadCount) && <SessionUnreadCount count={unreadCount} />}
</SessionIconButton>
</SessionScrollButtonDiv>
);
};

@ -1,11 +1,12 @@
import React, { useRef } from 'react';
import classNames from 'classnames';
import React, { useRef } from 'react';
import useKey from 'react-use/lib/useKey';
import { SessionIconButton } from './icon';
import { SessionButton, SessionButtonColor, SessionButtonType } from './basic/SessionButton';
import { StyledRootDialog } from './dialog/StyledRootDialog';
import { SessionFocusTrap } from './SessionFocusTrap';
export type SessionWrapperModalType = {
title?: string;
@ -17,7 +18,7 @@ export type SessionWrapperModalType = {
cancelText?: string;
showExitIcon?: boolean;
headerIconButtons?: Array<any>;
children: any;
children: React.ReactNode;
headerReverse?: boolean;
additionalClassName?: string;
};
@ -64,68 +65,70 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => {
};
return (
<StyledRootDialog
className={classNames('loki-dialog modal', additionalClassName || null)}
onClick={handleClick}
role="dialog"
>
<div className="session-confirm-wrapper">
<div ref={modalRef} className="session-modal">
{showHeader ? (
<div className={classNames('session-modal__header', headerReverse && 'reverse')}>
<div className="session-modal__header__close">
{showExitIcon ? (
<SessionIconButton
iconType="exit"
iconSize="small"
onClick={props.onClose}
dataTestId="modal-close-button"
/>
) : null}
<SessionFocusTrap>
<StyledRootDialog
className={classNames('loki-dialog modal', additionalClassName || null)}
onClick={handleClick}
role="dialog"
>
<div className="session-confirm-wrapper">
<div ref={modalRef} className="session-modal">
{showHeader ? (
<div className={classNames('session-modal__header', headerReverse && 'reverse')}>
<div className="session-modal__header__close">
{showExitIcon ? (
<SessionIconButton
iconType="exit"
iconSize="small"
onClick={props.onClose}
dataTestId="modal-close-button"
/>
) : null}
</div>
<div className="session-modal__header__title">{title}</div>
<div className="session-modal__header__icons">
{headerIconButtons
? headerIconButtons.map((iconItem: any) => {
return (
<SessionIconButton
key={iconItem.iconType}
iconType={iconItem.iconType}
iconSize={'large'}
iconRotation={iconItem.iconRotation}
onClick={iconItem.onClick}
/>
);
})
: null}
</div>
</div>
<div className="session-modal__header__title">{title}</div>
<div className="session-modal__header__icons">
{headerIconButtons
? headerIconButtons.map((iconItem: any) => {
return (
<SessionIconButton
key={iconItem.iconType}
iconType={iconItem.iconType}
iconSize={'large'}
iconRotation={iconItem.iconRotation}
onClick={iconItem.onClick}
/>
);
})
: null}
</div>
</div>
) : null}
) : null}
<div className="session-modal__body">
<div className="session-modal__centered">
{props.children}
<div className="session-modal__body">
<div className="session-modal__centered">
{props.children}
<div className="session-modal__button-group">
{onConfirm ? (
<SessionButton buttonType={SessionButtonType.Simple} onClick={props.onConfirm}>
{confirmText || window.i18n('ok')}
</SessionButton>
) : null}
{onClose && showClose ? (
<SessionButton
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={props.onClose}
>
{cancelText || window.i18n('close')}
</SessionButton>
) : null}
<div className="session-modal__button-group">
{onConfirm ? (
<SessionButton buttonType={SessionButtonType.Simple} onClick={props.onConfirm}>
{confirmText || window.i18n('ok')}
</SessionButton>
) : null}
{onClose && showClose ? (
<SessionButton
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={props.onClose}
>
{cancelText || window.i18n('close')}
</SessionButton>
) : null}
</div>
</div>
</div>
</div>
</div>
</div>
</StyledRootDialog>
</StyledRootDialog>
</SessionFocusTrap>
);
};

@ -28,7 +28,7 @@ export enum SessionButtonColor {
None = 'transparent',
}
const StyledButton = styled.div<{
const StyledButton = styled.button<{
color: string | undefined;
buttonType: SessionButtonType;
buttonShape: SessionButtonShape;

@ -1,9 +1,8 @@
import React, { SessionDataTestId, useState } from 'react';
import classNames from 'classnames';
import { Noop } from '../../types/Util';
import { useHTMLDirection } from '../../util/i18n';
import { SessionIconButton } from '../icon';
import { useHTMLDirection } from '../../util/i18n';
type Props = {
label?: string;
@ -46,7 +45,7 @@ const ErrorItem = (props: { error: string | undefined }) => {
);
};
const ShowHideButton = (props: { toggleForceShow: Noop }) => {
const ShowHideButton = (props: { toggleForceShow: () => void }) => {
const htmlDirection = useHTMLDirection();
const position = htmlDirection === 'ltr' ? { right: '0px' } : { left: '0px' };

@ -2,6 +2,11 @@ import React, { ChangeEvent, SessionDataTestId } from 'react';
import styled, { CSSProperties } from 'styled-components';
import { Flex } from './Flex';
const StyledButton = styled.button<{ disabled: boolean }>`
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
min-height: 30px;
`;
const StyledInput = styled.input<{
filledSize: number;
outlineOffset: number;
@ -9,7 +14,6 @@ const StyledInput = styled.input<{
}>`
opacity: 0;
position: absolute;
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
width: ${props => props.filledSize + props.outlineOffset}px;
height: ${props => props.filledSize + props.outlineOffset}px;
@ -75,7 +79,7 @@ export const SessionRadio = (props: SessionRadioProps) => {
inputDatatestId,
} = props;
const clickHandler = (e: ChangeEvent<any>) => {
const clickHandler = (e: React.SyntheticEvent<any>) => {
if (!disabled && onClick) {
// let something else catch the event if our click handler is not set
e.stopPropagation();
@ -87,37 +91,48 @@ export const SessionRadio = (props: SessionRadioProps) => {
const outlineOffset = 2;
return (
<Flex
container={true}
flexDirection={radioPosition === 'left' ? 'row' : 'row-reverse'}
justifyContent={radioPosition === 'left' ? 'flex-start' : 'flex-end'}
style={style}
<StyledButton
onKeyDown={e => {
if (e.code === 'Space') {
clickHandler(e);
}
}}
onClick={clickHandler}
disabled={disabled}
>
<StyledInput
type="radio"
name={inputName || ''}
value={value}
aria-checked={active}
checked={active}
onChange={clickHandler}
filledSize={filledSize * 2}
outlineOffset={outlineOffset}
disabled={disabled}
data-testid={inputDatatestId}
/>
<StyledLabel
role="button"
onClick={clickHandler}
filledSize={filledSize - 1}
outlineOffset={outlineOffset}
beforeMargins={beforeMargins}
aria-label={label}
disabled={disabled}
data-testid={labelDatatestId}
<Flex
container={true}
flexDirection={radioPosition === 'left' ? 'row' : 'row-reverse'}
justifyContent={radioPosition === 'left' ? 'flex-start' : 'flex-end'}
style={{ ...style, position: 'relative' }}
>
{label}
</StyledLabel>
</Flex>
<StyledInput
type="radio"
name={inputName || ''}
value={value}
aria-checked={active}
checked={active}
onChange={clickHandler}
tabIndex={-1} // clickHandler is on the parent button, so we need to skip this input while pressing Tab
filledSize={filledSize * 2}
outlineOffset={outlineOffset}
disabled={disabled}
data-testid={inputDatatestId}
/>
<StyledLabel
role="button"
onClick={clickHandler}
filledSize={filledSize - 1}
outlineOffset={outlineOffset}
beforeMargins={beforeMargins}
aria-label={label}
disabled={disabled}
data-testid={labelDatatestId}
>
{label}
</StyledLabel>
</Flex>
</StyledButton>
);
};

@ -41,6 +41,7 @@ const StyledYourSessionIDSelectable = styled.p`
font-weight: 300;
font-size: var(--font-size-sm);
color: var(--text-primary-color);
flex-shrink: 0;
`;
export const YourSessionIDSelectable = () => {

@ -355,7 +355,7 @@ const StyledCallWindowControls = styled.div<{ isFullScreen: boolean; makeVisible
margin-right: auto;
left: 0;
right: 0;
transition: all 0.25s ease-in-out;
transition: all var(--default-duration) ease-in-out;
display: flex;
justify-content: center;

@ -3,13 +3,13 @@ import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useConversationUsername } from '../../hooks/useParamSelector';
import { ed25519Str } from '../../session/onions/onionPath';
import { CallManager } from '../../session/utils';
import { callTimeoutMs } from '../../session/utils/calling/CallManager';
import { getHasIncomingCall, getHasIncomingCallFrom } from '../../state/selectors/call';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { ed25519Str } from '../../session/utils/String';
export const CallWindow = styled.div`
position: absolute;

@ -1,6 +1,5 @@
import classNames from 'classnames';
import React from 'react';
import { CSSProperties } from 'styled-components';
import {
useIsPrivate,
@ -28,12 +27,20 @@ export const ContactName = (props: Props) => {
const convoName = useNicknameOrProfileNameOrShortenedPubkey(pubkey);
const isPrivate = useIsPrivate(pubkey);
const shouldShowProfile = Boolean(convoName || profileName || name);
const commonStyles = {
'min-width': 0,
'text-overflow': 'ellipsis',
overflow: 'hidden',
} as React.CSSProperties;
const styles = (
boldProfileName
? {
fontWeight: 'bold',
...commonStyles,
}
: {}
: commonStyles
) as React.CSSProperties;
const textProfile = profileName || name || convoName || window.i18n('anonymous');
@ -42,15 +49,19 @@ export const ContactName = (props: Props) => {
className={classNames(prefix, compact && 'compact')}
dir="auto"
data-testid={`${prefix}__profile-name` as const}
style={{ textOverflow: 'inherit' }}
style={{
textOverflow: 'inherit',
display: 'flex',
flexDirection: 'row',
gap: 'var(--margins-xs)',
}}
>
{shouldShowProfile ? (
<span style={styles as CSSProperties} className={`${prefix}__profile-name`}>
<div style={styles} className={`${prefix}__profile-name`}>
<Emojify text={textProfile} sizeClass="small" isGroup={!isPrivate} />
</span>
</div>
) : null}
{shouldShowProfile ? ' ' : null}
{shouldShowPubkey ? <span className={`${prefix}__profile-number`}>{pubkey}</span> : null}
{shouldShowPubkey ? <div className={`${prefix}__profile-number`}>{pubkey}</div> : null}
</span>
);
};

@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React from 'react';
import styled from 'styled-components';
import {
@ -10,10 +10,10 @@ import {
isVideoAttachment,
} from '../../types/Attachment';
import { useIsMessageVisible } from '../../contexts/isMessageVisibleContext';
import { useMessageSelected } from '../../state/selectors';
import { THUMBNAIL_SIDE } from '../../types/attachments/VisualAttachment';
import { Image } from './Image';
import { IsMessageVisibleContext } from './message/message-content/MessageContent';
type Props = {
attachments: Array<AttachmentTypeWithPath>;
@ -46,7 +46,7 @@ const Row = (
totalAttachmentsCount,
selected,
} = props;
const isMessageVisible = useContext(IsMessageVisibleContext);
const isMessageVisible = useIsMessageVisible();
const moreMessagesOverlay = totalAttachmentsCount > 3;
const moreMessagesOverlayText = moreMessagesOverlay ? `+${totalAttachmentsCount - 3}` : undefined;

@ -61,7 +61,6 @@ import { ConversationMessageRequestButtons } from './MessageRequestButtons';
import { RightPanel, StyledRightPanelContainer } from './right-panel/RightPanel';
const DEFAULT_JPEG_QUALITY = 0.85;
interface State {
isDraggingFile: boolean;
}
@ -344,6 +343,7 @@ export class SessionConversation extends React.Component<Props, State> {
}
break;
default:
break;
}
}
}

@ -1,9 +1,9 @@
import React, { useContext, useLayoutEffect } from 'react';
import React, { useLayoutEffect } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useScrollToLoadedMessage } from '../../contexts/ScrollToLoadedMessage';
import { getQuotedMessageToAnimate } from '../../state/selectors/conversations';
import { isDarkTheme } from '../../state/selectors/theme';
import { ScrollToLoadedMessageContext } from './SessionMessagesListContainer';
const LastSeenBar = styled.div`
height: 2px;
@ -52,7 +52,7 @@ export const SessionLastSeenIndicator = (props: {
const darkMode = useSelector(isDarkTheme);
// if this unread-indicator is not unique it's going to cause issues
const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate);
const scrollToLoadedMessage = useContext(ScrollToLoadedMessageContext);
const scrollToLoadedMessage = useScrollToLoadedMessage();
const { messageId, didScroll, setDidScroll } = props;

@ -26,9 +26,10 @@ import { Message } from './message/message-item/Message';
import { MessageRequestResponse } from './message/message-item/MessageRequestResponse';
import { CallNotification } from './message/message-item/notification-bubble/CallNotification';
import { DataExtractionNotification } from './message/message-item/DataExtractionNotification';
import { IsDetailMessageViewContext } from '../../contexts/isDetailViewContext';
import { SessionLastSeenIndicator } from './SessionLastSeenIndicator';
import { TimerNotification } from './TimerNotification';
import { DataExtractionNotification } from './message/message-item/DataExtractionNotification';
import { InteractionNotification } from './message/message-item/InteractionNotification';
function isNotTextboxEvent(e: KeyboardEvent) {
@ -98,7 +99,7 @@ export const SessionMessagesList = (props: {
}
return (
<>
<IsDetailMessageViewContext.Provider value={false}>
{messagesProps.map(messageProps => {
const messageId = messageProps.message.props.messageId;
const unreadIndicator = messageProps.showUnreadIndicator ? (
@ -170,6 +171,6 @@ export const SessionMessagesList = (props: {
return [<Message messageId={messageId} key={messageId} />, ...componentToMerge];
})}
</>
</IsDetailMessageViewContext.Provider>
);
};

@ -15,6 +15,10 @@ import {
} from '../../state/ducks/conversations';
import { SessionScrollButton } from '../SessionScrollButton';
import {
ScrollToLoadedMessageContext,
ScrollToLoadedReasons,
} from '../../contexts/ScrollToLoadedMessage';
import { StateType } from '../../state/reducer';
import {
getQuotedMessageToAnimate,
@ -30,17 +34,6 @@ export type SessionMessageListProps = {
};
export const messageContainerDomID = 'messages-container';
export type ScrollToLoadedReasons =
| 'quote-or-search-result'
| 'go-to-bottom'
| 'unread-indicator'
| 'load-more-top'
| 'load-more-bottom';
export const ScrollToLoadedMessageContext = React.createContext(
(_loadedMessageIdToScrollTo: string, _reason: ScrollToLoadedReasons) => {}
);
type Props = SessionMessageListProps & {
conversationKey?: string;
messagesProps: Array<SortedMessageModelProps>;

@ -1,7 +1,6 @@
import React from 'react';
import styled from 'styled-components';
import { SessionIconButton } from '../../icon';
import { Noop } from '../../../types/Util';
const StyledChatButtonContainer = styled.div`
.session-icon-button {
@ -15,7 +14,7 @@ const StyledChatButtonContainer = styled.div`
}
`;
export const AddStagedAttachmentButton = (props: { onClick: Noop }) => {
export const AddStagedAttachmentButton = (props: { onClick: () => void }) => {
return (
<StyledChatButtonContainer>
<SessionIconButton
@ -32,7 +31,7 @@ export const AddStagedAttachmentButton = (props: { onClick: Noop }) => {
);
};
export const StartRecordingButton = (props: { onClick: Noop }) => {
export const StartRecordingButton = (props: { onClick: () => void }) => {
return (
<StyledChatButtonContainer>
<SessionIconButton
@ -50,7 +49,7 @@ export const StartRecordingButton = (props: { onClick: Noop }) => {
};
// eslint-disable-next-line react/display-name
export const ToggleEmojiButton = React.forwardRef<HTMLDivElement, { onClick: Noop }>(
export const ToggleEmojiButton = React.forwardRef<HTMLButtonElement, { onClick: () => void }>(
(props, ref) => {
return (
<StyledChatButtonContainer>
@ -70,7 +69,7 @@ export const ToggleEmojiButton = React.forwardRef<HTMLDivElement, { onClick: Noo
}
);
export const SendMessageButton = (props: { onClick: Noop }) => {
export const SendMessageButton = (props: { onClick: () => void }) => {
return (
<StyledChatButtonContainer className="send-message-button">
<SessionIconButton

@ -1,7 +1,7 @@
import React from 'react';
import { SuggestionDataItem } from 'react-mentions';
import { MemberListItem } from '../../MemberListItem';
import { HTMLDirection } from '../../../util/i18n';
import { MemberListItem } from '../../MemberListItem';
const listRTLStyle = { position: 'absolute', bottom: '0px', right: '100%' };
@ -21,7 +21,7 @@ export const styleForCompositionBoxSuggestions = (dir: HTMLDirection = 'ltr') =>
paddingBottom: '5px',
backgroundColor: 'var(--suggestions-background-color)',
color: 'var(--suggestions-text-color)',
transition: '0.25s',
transition: 'var(--default-duration)',
'&focused': {
backgroundColor: 'var(--suggestions-background-hover-color)',

@ -1,9 +1,9 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
deleteMessagesById,
deleteMessagesByIdForEveryone,
} from '../../../interactions/conversations/unsendingInteractions';
import useKey from 'react-use/lib/useKey';
import { deleteMessagesForX } from '../../../interactions/conversations/unsendingInteractions';
import { resetSelectedMessageIds } from '../../../state/ducks/conversations';
import { getSelectedMessageIds } from '../../../state/selectors/conversations';
import {
@ -17,15 +17,7 @@ import {
SessionButtonType,
} from '../../basic/SessionButton';
import { SessionIconButton } from '../../icon';
function onDeleteSelectedMessagesForEveryone(
selectedConversationKey: string,
selectedMessageIds: Array<string>
) {
if (selectedConversationKey) {
void deleteMessagesByIdForEveryone(selectedMessageIds, selectedConversationKey);
}
}
import { SessionFocusTrap } from '../../SessionFocusTrap';
export const SelectionOverlay = () => {
const selectedMessageIds = useSelector(getSelectedMessageIds);
@ -36,35 +28,72 @@ export const SelectionOverlay = () => {
function onCloseOverlay() {
dispatch(resetSelectedMessageIds());
}
/**
* This is a duplicate with the onKeyDown of SessionConversation.
* At some point we'll make a global handler to deal with the key presses
* and handle them depending on what is visible, but that's not part of this PR
*/
useKey(
shouldProcess => {
return (
shouldProcess.code === 'Escape' ||
shouldProcess.code === 'Backspace' ||
shouldProcess.code === 'Delete'
);
},
event => {
const selectionMode = !!selectedMessageIds.length;
switch (event.key) {
case 'Escape':
if (selectionMode) {
onCloseOverlay();
}
return true;
case 'Backspace':
case 'Delete':
if (selectionMode && selectedConversationKey) {
void deleteMessagesForX(selectedMessageIds, selectedConversationKey, isPublic);
}
return true;
default:
}
return false;
}
);
const isOnlyServerDeletable = isPublic;
// `enforceDeleteServerSide` should check for message statuses too, but when we have multiple selected,
// some might be sent and some in an error state. We default to trying to delete all of them server side first,
// which might fail. If that fails, the user will need to do a delete for all the ones sent already, and a manual delete
// for each ones which is in an error state.
const enforceDeleteServerSide = isPublic;
const classNameAndId = 'message-selection-overlay';
return (
<div className="message-selection-overlay">
<div className="close-button">
<SessionIconButton iconType="exit" iconSize="medium" onClick={onCloseOverlay} />
</div>
<SessionFocusTrap>
<div className={classNameAndId} id={classNameAndId}>
<div className="close-button">
<SessionIconButton iconType="exit" iconSize="medium" onClick={onCloseOverlay} />
</div>
<div className="button-group">
<SessionButton
buttonColor={SessionButtonColor.Danger}
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
text={window.i18n('delete')}
onClick={() => {
if (selectedConversationKey) {
if (isOnlyServerDeletable) {
void onDeleteSelectedMessagesForEveryone(
<div className="button-group">
<SessionButton
buttonColor={SessionButtonColor.Danger}
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
text={window.i18n('delete')}
onClick={async () => {
if (selectedConversationKey) {
await deleteMessagesForX(
selectedMessageIds,
selectedConversationKey,
selectedMessageIds
enforceDeleteServerSide
);
} else {
void deleteMessagesById(selectedMessageIds, selectedConversationKey);
}
}
}}
/>
}}
/>
</div>
</div>
</div>
</SessionFocusTrap>
);
};

@ -19,7 +19,7 @@ import {
import {
AttachmentType,
AttachmentTypeWithPath,
canDisplayImage,
canDisplayImagePreview,
getExtensionForDisplay,
hasImage,
hasVideoScreenshot,
@ -96,7 +96,7 @@ export const MessageAttachment = (props: Props) => {
(e: any) => {
e.stopPropagation();
e.preventDefault();
if (!attachmentProps?.attachments?.length) {
if (!attachmentProps?.attachments?.length || attachmentProps?.attachments[0]?.pending) {
return;
}
@ -131,7 +131,7 @@ export const MessageAttachment = (props: Props) => {
}
const firstAttachment = attachments[0];
const displayImage = canDisplayImage(attachments);
const displayImage = canDisplayImagePreview(attachments);
if (!isTrustedForAttachmentDownload) {
return <ClickToTrustSender messageId={messageId} />;
@ -186,6 +186,7 @@ export const MessageAttachment = (props: Props) => {
highlight={highlight}
selected={selected}
className={'module-message__generic-attachment'}
onClick={onClickOnGenericAttachment}
>
{pending ? (
<div className="module-message__generic-attachment__spinner-container">
@ -193,11 +194,7 @@ export const MessageAttachment = (props: Props) => {
</div>
) : (
<div className="module-message__generic-attachment__icon-container">
<div
role="button"
className="module-message__generic-attachment__icon"
onClick={onClickOnGenericAttachment}
>
<div role="button" className="module-message__generic-attachment__icon">
{extension ? (
<div className="module-message__generic-attachment__icon__extension">{extension}</div>
) : null}

@ -38,10 +38,10 @@ export type MessageAvatarSelectorProps = Pick<
'sender' | 'isSenderAdmin' | 'lastMessageOfSeries'
>;
type Props = { messageId: string; hideAvatar: boolean; isPrivate: boolean; isDetailView?: boolean };
type Props = { messageId: string; isPrivate: boolean };
export const MessageAvatar = (props: Props) => {
const { messageId, hideAvatar, isPrivate, isDetailView } = props;
const { messageId, isPrivate } = props;
const dispatch = useDispatch();
const selectedConvoKey = useSelectedConversationKey();
@ -135,13 +135,9 @@ export const MessageAvatar = (props: Props) => {
// The styledAvatar, when rendered needs to have a width with margins included of var(--width-avatar-group-msg-list).
// This is so that the other message is still aligned when the avatar is not rendered (we need to make up for the space used by the avatar, and we use a margin of width-avatar-group-msg-list)
return (
<StyledAvatar
style={{
visibility: hideAvatar ? 'hidden' : undefined,
}}
>
<StyledAvatar>
<Avatar size={AvatarSize.S} onAvatarClick={onMessageAvatarClick} pubkey={sender} />
{!isDetailView && isSenderAdmin ? <CrownIcon /> : null}
{isSenderAdmin ? <CrownIcon /> : null}
</StyledAvatar>
);
};

@ -33,7 +33,7 @@ const renderNewLines: RenderTextCallbackType = ({ text: textWithNewLines, key, i
<AddNewLines
key={key}
text={textWithNewLines}
renderNonNewLine={isGroup ? renderMentions : renderTextDefault}
renderNonNewLine={renderMentions}
isGroup={isGroup}
/>
);

@ -1,10 +1,13 @@
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import moment from 'moment';
import React, { createContext, useCallback, useContext, useLayoutEffect, useState } from 'react';
import React, { useCallback, useLayoutEffect, useState } from 'react';
import { InView } from 'react-intersection-observer';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useScrollToLoadedMessage } from '../../../../contexts/ScrollToLoadedMessage';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { IsMessageVisibleContext } from '../../../../contexts/isMessageVisibleContext';
import { MessageModelType, MessageRenderingProps } from '../../../../models/messageType';
import { StateType } from '../../../../state/reducer';
import {
@ -18,8 +21,7 @@ import {
getShouldHighlightMessage,
} from '../../../../state/selectors/conversations';
import { useSelectedIsPrivate } from '../../../../state/selectors/selectedConversation';
import { canDisplayImage } from '../../../../types/Attachment';
import { ScrollToLoadedMessageContext } from '../../SessionMessagesListContainer';
import { canDisplayImagePreview } from '../../../../types/Attachment';
import { MessageAttachment } from './MessageAttachment';
import { MessageAvatar } from './MessageAvatar';
import { MessageHighlighter } from './MessageHighlighter';
@ -34,7 +36,6 @@ export type MessageContentSelectorProps = Pick<
type Props = {
messageId: string;
isDetailView?: boolean;
};
// TODO not too sure what is this doing? It is not preventDefault()
@ -75,13 +76,13 @@ const StyledMessageOpaqueContent = styled(MessageHighlighter)<{
${props => props.selected && `box-shadow: var(--drop-shadow);`}
`;
export const IsMessageVisibleContext = createContext(false);
const StyledAvatarContainer = styled.div`
align-self: flex-end;
`;
export const MessageContent = (props: Props) => {
const isDetailView = useIsDetailMessageView();
const [highlight, setHighlight] = useState(false);
const [didScroll, setDidScroll] = useState(false);
const contentProps = useSelector((state: StateType) =>
@ -90,9 +91,9 @@ export const MessageContent = (props: Props) => {
const isDeleted = useMessageIsDeleted(props.messageId);
const [isMessageVisible, setMessageIsVisible] = useState(false);
const scrollToLoadedMessage = useContext(ScrollToLoadedMessageContext);
const scrollToLoadedMessage = useScrollToLoadedMessage();
const selectedIsPrivate = useSelectedIsPrivate();
const hideAvatar = useHideAvatarInMsgList(props.messageId);
const hideAvatar = useHideAvatarInMsgList(props.messageId, isDetailView);
const [imageBroken, setImageBroken] = useState(false);
@ -153,7 +154,7 @@ export const MessageContent = (props: Props) => {
const toolTipTitle = moment(serverTimestamp || timestamp).format('llll');
const isDetailViewAndSupportsAttachmentCarousel =
props.isDetailView && canDisplayImage(attachments);
isDetailView && canDisplayImagePreview(attachments);
return (
<StyledMessageContent
@ -163,14 +164,11 @@ export const MessageContent = (props: Props) => {
title={toolTipTitle}
msgDirection={direction}
>
<StyledAvatarContainer>
<MessageAvatar
messageId={props.messageId}
hideAvatar={hideAvatar}
isPrivate={selectedIsPrivate}
isDetailView={props.isDetailView}
/>
</StyledAvatarContainer>
{hideAvatar ? null : (
<StyledAvatarContainer>
<MessageAvatar messageId={props.messageId} isPrivate={selectedIsPrivate} />
</StyledAvatarContainer>
)}
<InView
id={`inview-content-${props.messageId}`}
@ -183,6 +181,7 @@ export const MessageContent = (props: Props) => {
display: 'flex',
flexDirection: 'column',
gap: 'var(--margins-xs)',
maxWidth: '100%',
}}
>
<IsMessageVisibleContext.Provider value={isMessageVisible}>

@ -2,6 +2,7 @@ import classNames from 'classnames';
import React, { SessionDataTestId, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { replyToMessage } from '../../../../interactions/conversationInteractions';
import { MessageRenderingProps } from '../../../../models/messageType';
import { toggleSelectedMessageId } from '../../../../state/ducks/conversations';
@ -29,30 +30,33 @@ export type MessageContentWithStatusSelectorProps = { isGroup: boolean } & Pick<
type Props = {
messageId: string;
ctxMenuID: string;
isDetailView?: boolean;
dataTestId: SessionDataTestId;
enableReactions: boolean;
};
const StyledMessageContentContainer = styled.div<{ isIncoming: boolean }>`
const StyledMessageContentContainer = styled.div<{ isIncoming: boolean; isDetailView: boolean }>`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')};
padding-left: ${props => (props.isIncoming ? 0 : '25%')};
padding-right: ${props => (props.isIncoming ? '25%' : 0)};
padding-left: ${props => (props.isDetailView || props.isIncoming ? 0 : '25%')};
padding-right: ${props => (props.isDetailView || !props.isIncoming ? 0 : '25%')};
width: 100%;
max-width: '100%';
margin-right: var(--margins-md);
`;
const StyledMessageWithAuthor = styled.div`
max-width: '100%';
max-width: 100%;
display: flex;
flex-direction: column;
min-width: 0;
gap: var(--margins-xs);
`;
export const MessageContentWithStatuses = (props: Props) => {
const isDetailView = useIsDetailMessageView();
const contentProps = useSelector((state: StateType) =>
getMessageContentWithStatusesSelectorProps(state, props.messageId)
);
@ -91,7 +95,7 @@ export const MessageContentWithStatuses = (props: Props) => {
}
};
const { messageId, ctxMenuID, isDetailView = false, dataTestId, enableReactions } = props;
const { messageId, ctxMenuID, dataTestId, enableReactions } = props;
const [popupReaction, setPopupReaction] = useState('');
if (!contentProps) {
@ -119,6 +123,7 @@ export const MessageContentWithStatuses = (props: Props) => {
return (
<StyledMessageContentContainer
isIncoming={isIncoming}
isDetailView={isDetailView}
onMouseLeave={() => {
setPopupReaction('');
}}
@ -127,21 +132,22 @@ export const MessageContentWithStatuses = (props: Props) => {
messageId={messageId}
className={classNames('module-message', `module-message--${direction}`)}
role={'button'}
isDetailView={isDetailView}
onClick={onClickOnMessageOuterContainer}
onDoubleClickCapture={onDoubleClickReplyToMessage}
dataTestId={dataTestId}
>
<Flex container={true} flexDirection="column" flexShrink={0} alignItems="flex-end">
<Flex
container={true}
flexDirection="column"
flexShrink={0}
alignItems="flex-end"
maxWidth="100%"
>
<StyledMessageWithAuthor>
{!isDetailView && <MessageAuthorText messageId={messageId} />}
<MessageContent messageId={messageId} isDetailView={isDetailView} />
<MessageContent messageId={messageId} />
</StyledMessageWithAuthor>
<MessageStatus
dataTestId="msg-status"
messageId={messageId}
isDetailView={isDetailView}
/>
<MessageStatus dataTestId="msg-status" messageId={messageId} />
</Flex>
{!isDeleted && (
<MessageContextMenu
@ -160,7 +166,6 @@ export const MessageContentWithStatuses = (props: Props) => {
setPopupReaction={setPopupReaction}
onPopupClick={handlePopupClick}
noAvatar={hideAvatar}
isDetailView={isDetailView}
/>
) : null}
</StyledMessageContentContainer>

@ -10,10 +10,7 @@ import { Data } from '../../../../data/data';
import { MessageInteraction } from '../../../../interactions';
import { replyToMessage } from '../../../../interactions/conversationInteractions';
import {
deleteMessagesById,
deleteMessagesByIdForEveryone,
} from '../../../../interactions/conversations/unsendingInteractions';
import { deleteMessagesForX } from '../../../../interactions/conversations/unsendingInteractions';
import {
addSenderAsModerator,
removeSenderFromModerator,
@ -94,17 +91,15 @@ const DeleteItem = ({ messageId }: { messageId: string }) => {
const isDeletable = useMessageIsDeletable(messageId);
const isDeletableForEveryone = useMessageIsDeletableForEveryone(messageId);
const messageStatus = useMessageStatus(messageId);
const enforceDeleteServerSide = isPublic && messageStatus !== 'error';
const onDelete = useCallback(() => {
if (convoId) {
if (!isPublic && isDeletable) {
void deleteMessagesById([messageId], convoId);
}
if (isPublic && isDeletableForEveryone) {
void deleteMessagesByIdForEveryone([messageId], convoId);
}
void deleteMessagesForX([messageId], convoId, enforceDeleteServerSide);
}
}, [convoId, isDeletable, isDeletableForEveryone, isPublic, messageId]);
}, [convoId, enforceDeleteServerSide, messageId]);
if (!convoId || (isPublic && !isDeletableForEveryone) || (!isPublic && !isDeletable)) {
return null;

@ -1,6 +1,7 @@
import { isEmpty, toNumber } from 'lodash';
import React from 'react';
import { useSelector } from 'react-redux';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { Data } from '../../../../data/data';
import { MessageRenderingProps } from '../../../../models/messageType';
import { ToastUtils } from '../../../../session/utils';
@ -19,6 +20,7 @@ export type MessageQuoteSelectorProps = Pick<MessageRenderingProps, 'quote' | 'd
export const MessageQuote = (props: Props) => {
const selected = useSelector((state: StateType) => getMessageQuoteProps(state, props.messageId));
const direction = useMessageDirection(props.messageId);
const isMessageDetailView = useIsDetailMessageView();
if (!selected || isEmpty(selected)) {
return null;
@ -38,6 +40,10 @@ export const MessageQuote = (props: Props) => {
event.preventDefault();
event.stopPropagation();
if (isMessageDetailView) {
return;
}
if (!quote) {
ToastUtils.pushOriginalNotFound();
window.log.warn('onQuoteClick: quote not valid');

@ -1,4 +1,4 @@
import React, { ReactElement } from 'react';
import React from 'react';
import styled from 'styled-components';
import { isEmpty } from 'lodash';
@ -160,7 +160,7 @@ const ExpiresInItem = ({ expirationTimestamp }: { expirationTimestamp?: number |
);
};
export const MessageReactBar = ({ action, additionalAction, messageId }: Props): ReactElement => {
export const MessageReactBar = ({ action, additionalAction, messageId }: Props) => {
const recentReactions = getRecentReactions();
const expirationTimestamp = useIsRenderedExpiresInItem(messageId);

@ -1,6 +1,7 @@
import { isEmpty, isEqual } from 'lodash';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { useMessageReactsPropsById } from '../../../../hooks/useParamSelector';
import { MessageRenderingProps } from '../../../../models/messageType';
import { REACT_LIMIT } from '../../../../session/constants';
@ -27,7 +28,7 @@ export const StyledMessageReactionsContainer = styled(Flex)<{
}
// MessageAvatar width + margin-inline-end
${props => !props.noAvatar && 'margin-inline-start: calc(36px + 20px);'}
${props => !props.noAvatar && 'margin-inline-start: var(--width-avatar-group-msg-list);'}
`;
export const StyledMessageReactions = styled(Flex)<{ fullWidth: boolean }>`
@ -147,10 +148,11 @@ type Props = {
inModal?: boolean;
onSelected?: (emoji: string) => boolean;
noAvatar: boolean;
isDetailView?: boolean;
};
export const MessageReactions = (props: Props) => {
const isDetailView = useIsDetailMessageView();
const {
messageId,
hasReactLimit = true,
@ -161,7 +163,6 @@ export const MessageReactions = (props: Props) => {
inModal = false,
onSelected,
noAvatar,
isDetailView,
} = props;
const [reactions, setReactions] = useState<SortedReactionList>([]);

@ -5,14 +5,14 @@ import styled from 'styled-components';
import { useMessageExpirationPropsById } from '../../../../hooks/useParamSelector';
import { useMessageStatus } from '../../../../state/selectors';
import { getMostRecentMessageId } from '../../../../state/selectors/conversations';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { getMostRecentOutgoingMessageId } from '../../../../state/selectors/conversations';
import { useSelectedIsGroupOrCommunity } from '../../../../state/selectors/selectedConversation';
import { SpacerXS } from '../../../basic/Text';
import { SessionIcon, SessionIconType } from '../../../icon';
import { ExpireTimer } from '../../ExpireTimer';
type Props = {
isDetailView: boolean;
messageId: string;
dataTestId: SessionDataTestId;
};
@ -30,7 +30,9 @@ type Props = {
* - if the message is incoming: do not show anything (3)
* - if the message is outgoing: show the text for the last message, or a message sending, or in the error state. (4)
*/
export const MessageStatus = ({ isDetailView, messageId, dataTestId }: Props) => {
export const MessageStatus = ({ messageId, dataTestId }: Props) => {
const isDetailView = useIsDetailMessageView();
const status = useMessageStatus(messageId);
const selected = useMessageExpirationPropsById(messageId);
@ -120,10 +122,9 @@ function useIsExpiring(messageId: string) {
);
}
function useIsMostRecentMessage(messageId: string) {
const mostRecentMessageId = useSelector(getMostRecentMessageId);
const isMostRecentMessage = mostRecentMessageId === messageId;
return isMostRecentMessage;
function useIsMostRecentOutgoingMessage(messageId: string) {
const mostRecentOutgoingMessageId = useSelector(getMostRecentOutgoingMessageId);
return mostRecentOutgoingMessageId === messageId;
}
function MessageStatusExpireTimer(props: Pick<Props, 'messageId'>) {
@ -178,11 +179,11 @@ function IconForExpiringMessageId({
const MessageStatusSent = ({ dataTestId, messageId }: Omit<Props, 'isDetailView'>) => {
const isExpiring = useIsExpiring(messageId);
const isMostRecentMessage = useIsMostRecentMessage(messageId);
const isMostRecentOutgoingMessage = useIsMostRecentOutgoingMessage(messageId);
const isGroup = useSelectedIsGroupOrCommunity();
// we hide a "sent" message status which is not expiring except for the most recent message
if (!isExpiring && !isMostRecentMessage) {
// we hide the "sent" message status for a non-expiring messages unless it's the most recent outgoing message
if (!isExpiring && !isMostRecentOutgoingMessage) {
return null;
}
return (
@ -206,10 +207,10 @@ const MessageStatusRead = ({
const isExpiring = useIsExpiring(messageId);
const isGroup = useSelectedIsGroupOrCommunity();
const isMostRecentMessage = useIsMostRecentMessage(messageId);
const isMostRecentOutgoingMessage = useIsMostRecentOutgoingMessage(messageId);
// we hide an outgoing "read" message status which is not expiring except for the most recent message
if (!isIncoming && !isExpiring && !isMostRecentMessage) {
if (!isIncoming && !isExpiring && !isMostRecentOutgoingMessage) {
return null;
}

@ -3,10 +3,10 @@ import React, { MouseEvent, useState } from 'react';
import { isEmpty } from 'lodash';
import styled from 'styled-components';
import { useIsMessageSelectionMode } from '../../../../../state/selectors/selectedConversation';
import * as MIME from '../../../../../types/MIME';
import { QuoteAuthor } from './QuoteAuthor';
import { QuoteIconContainer } from './QuoteIconContainer';
import { QuoteText } from './QuoteText';
import * as MIME from '../../../../../types/MIME';
const StyledQuoteContainer = styled.div`
min-width: 300px; // if the quoted content is small it doesn't look very good so we set a minimum

@ -2,9 +2,9 @@ import React from 'react';
import styled from 'styled-components';
import { useQuoteAuthorName } from '../../../../../hooks/useParamSelector';
import { PubKey } from '../../../../../session/types';
import { useSelectedIsPublic } from '../../../../../state/selectors/selectedConversation';
import { ContactName } from '../../../ContactName';
import { QuoteProps } from './Quote';
import { useSelectedIsPublic } from '../../../../../state/selectors/selectedConversation';
const StyledQuoteAuthor = styled.div<{ isIncoming: boolean }>`
color: ${props =>
@ -18,6 +18,7 @@ const StyledQuoteAuthor = styled.div<{ isIncoming: boolean }>`
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.module-contact-name {
font-weight: bold;
}

@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useInterval, useMount } from 'react-use';
import styled from 'styled-components';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { Data } from '../../../../data/data';
import { useMessageExpirationPropsById } from '../../../../hooks/useParamSelector';
import { MessageModelType } from '../../../../models/messageType';
@ -84,7 +85,6 @@ export interface ExpirableReadableMessageProps
extends Omit<ReadableMessageProps, 'receivedAt' | 'isUnread'> {
messageId: string;
isControlMessage?: boolean;
isDetailView?: boolean;
}
function ExpireTimerControlMessage({
@ -109,6 +109,7 @@ function ExpireTimerControlMessage({
export const ExpirableReadableMessage = (props: ExpirableReadableMessageProps) => {
const selected = useMessageExpirationPropsById(props.messageId);
const isDetailView = useIsDetailMessageView();
const { isControlMessage, onClick, onDoubleClickCapture, role, dataTestId } = props;
@ -125,11 +126,18 @@ export const ExpirableReadableMessage = (props: ExpirableReadableMessageProps) =
return null;
}
const { messageId, direction, receivedAt, isUnread, expirationDurationMs, expirationTimestamp } =
selected;
const {
messageId,
direction: _direction,
receivedAt,
isUnread,
expirationDurationMs,
expirationTimestamp,
} = selected;
// NOTE we want messages on the left in the message detail view regardless of direction
const isIncoming = props.isDetailView ? true : direction === 'incoming';
const direction = isDetailView ? 'incoming' : _direction;
const isIncoming = direction === 'incoming';
return (
<StyledReadableMessage

@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { contextMenu } from 'react-contexify';
import { useSelector } from 'react-redux';
import styled, { keyframes } from 'styled-components';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { MessageRenderingProps } from '../../../../models/messageType';
import { ConvoHub } from '../../../../session/conversations';
import { StateType } from '../../../../state/reducer';
@ -29,7 +30,6 @@ export type GenericReadableMessageSelectorProps = Pick<
type Props = {
messageId: string;
ctxMenuID: string;
isDetailView?: boolean;
};
const highlightedMessageAnimation = keyframes`
@ -40,8 +40,8 @@ const highlightedMessageAnimation = keyframes`
const StyledReadableMessage = styled.div<{
selected: boolean;
isDetailView: boolean;
isRightClicked: boolean;
isDetailView?: boolean;
}>`
display: flex;
align-items: center;
@ -64,7 +64,9 @@ const StyledReadableMessage = styled.div<{
`;
export const GenericReadableMessage = (props: Props) => {
const { ctxMenuID, messageId, isDetailView } = props;
const isDetailView = useIsDetailMessageView();
const { ctxMenuID, messageId } = props;
const [enableReactions, setEnableReactions] = useState(true);
@ -148,7 +150,6 @@ export const GenericReadableMessage = (props: Props) => {
<MessageContentWithStatuses
ctxMenuID={ctxMenuID}
messageId={messageId}
isDetailView={isDetailView}
dataTestId={'message-content'}
enableReactions={enableReactions}
/>

@ -12,7 +12,6 @@ export const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = THUMBNAIL_SIDE;
type Props = {
messageId: string;
isDetailView?: boolean; // when the detail is shown for a message, we disable click and some other stuff
};
export const Message = (props: Props) => {
@ -26,11 +25,5 @@ export const Message = (props: Props) => {
return null;
}
return (
<GenericReadableMessage
ctxMenuID={ctxMenuID}
messageId={props.messageId}
isDetailView={props.isDetailView}
/>
);
return <GenericReadableMessage ctxMenuID={ctxMenuID} messageId={props.messageId} />;
};

@ -39,7 +39,7 @@ export const MessageRequestResponse = (props: PropsForMessageRequestResponse) =>
id={`msg-${messageId}`}
>
<SpacerSM />
<Text text={msgText} subtle={true} ellipsisOverflow={true} />
<Text text={msgText} subtle={true} ellipsisOverflow={false} textAlign="center" />
</Flex>
</ReadableMessage>
);

@ -3,12 +3,12 @@ import React, {
MouseEventHandler,
SessionDataTestId,
useCallback,
useContext,
useLayoutEffect,
useState,
} from 'react';
import { InView } from 'react-intersection-observer';
import { useDispatch, useSelector } from 'react-redux';
import { useScrollToLoadedMessage } from '../../../../contexts/ScrollToLoadedMessage';
import { Data } from '../../../../data/data';
import { useHasUnread } from '../../../../hooks/useParamSelector';
import { ConvoHub } from '../../../../session/conversations';
@ -28,7 +28,6 @@ import {
} from '../../../../state/selectors/conversations';
import { getIsAppFocused } from '../../../../state/selectors/section';
import { useSelectedConversationKey } from '../../../../state/selectors/selectedConversation';
import { ScrollToLoadedMessageContext } from '../../SessionMessagesListContainer';
export type ReadableMessageProps = {
children: React.ReactNode;
@ -95,7 +94,7 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
const [didScroll, setDidScroll] = useState(false);
const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate);
const scrollToLoadedMessage = useContext(ScrollToLoadedMessageContext);
const scrollToLoadedMessage = useScrollToLoadedMessage();
// if this unread-indicator is rendered,
// we want to scroll here only if the conversation was not opened to a specific message

@ -13,7 +13,7 @@ export const StyledRightPanelContainer = styled.div`
height: var(--right-panel-height);
right: 0vw;
transition: transform 0.3s ease-in-out;
transition: transform var(--default-duration) ease-in-out;
transform: translateX(100%);
will-change: transform;
width: var(--right-panel-width);
@ -21,11 +21,13 @@ export const StyledRightPanelContainer = styled.div`
background-color: var(--background-primary-color);
border-left: 1px solid var(--border-color);
visibility: hidden;
&.show {
transform: none;
transition: transform 0.3s ease-in-out;
transition: transform var(--default-duration) ease-in-out;
z-index: 3;
visibility: visible;
}
`;

@ -10,6 +10,7 @@ import { getMessageInfoId } from '../../../../../state/selectors/conversations';
import { Flex } from '../../../../basic/Flex';
import { Header, HeaderTitle, StyledScrollContainer } from '../components';
import { IsDetailMessageViewContext } from '../../../../../contexts/isDetailViewContext';
import { Data } from '../../../../../data/data';
import { useRightOverlayMode } from '../../../../../hooks/useUI';
import {
@ -28,7 +29,7 @@ import {
useMessageTimestamp,
} from '../../../../../state/selectors';
import { useSelectedConversationKey } from '../../../../../state/selectors/selectedConversation';
import { canDisplayImage } from '../../../../../types/Attachment';
import { canDisplayImagePreview } from '../../../../../types/Attachment';
import { isAudio } from '../../../../../types/MIME';
import {
getAudioDuration,
@ -71,9 +72,11 @@ const MessageBody = ({
}
return (
<StyledMessageBody>
<Message messageId={messageId} isDetailView={true} />
</StyledMessageBody>
<IsDetailMessageViewContext.Provider value={true}>
<StyledMessageBody>
<Message messageId={messageId} />
</StyledMessageBody>
</IsDetailMessageViewContext.Provider>
);
};
@ -219,7 +222,7 @@ export const OverlayMessageInfo = () => {
const { errors, attachments } = messageInfo;
const hasAttachments = attachments && attachments.length > 0;
const supportsAttachmentCarousel = canDisplayImage(attachments);
const supportsAttachmentCarousel = canDisplayImagePreview(attachments);
const hasErrors = errors && errors.length > 0;
const handleChangeAttachment = (changeDirection: 1 | -1) => {

@ -2,7 +2,7 @@ import React from 'react';
import styled from 'styled-components';
import { MessageInfoLabel } from '.';
import { useConversationUsername } from '../../../../../../hooks/useParamSelector';
import { Avatar, AvatarSize } from '../../../../../avatar/Avatar';
import { Avatar, AvatarSize, CrownIcon } from '../../../../../avatar/Avatar';
const StyledFromContainer = styled.div`
display: flex;
@ -30,8 +30,12 @@ const StyledMessageInfoAuthor = styled.div`
font-size: var(--font-size-lg);
`;
export const MessageFrom = (props: { sender: string }) => {
const { sender } = props;
const StyledAvatar = styled.div`
position: relative;
`;
export const MessageFrom = (props: { sender: string; isSenderAdmin: boolean }) => {
const { sender, isSenderAdmin } = props;
const profileName = useConversationUsername(sender);
const from = window.i18n('from');
@ -39,7 +43,10 @@ export const MessageFrom = (props: { sender: string }) => {
<StyledMessageInfoAuthor>
<MessageInfoLabel>{from}</MessageInfoLabel>
<StyledFromContainer>
<Avatar size={AvatarSize.M} pubkey={sender} onAvatarClick={undefined} />
<StyledAvatar>
<Avatar size={AvatarSize.M} pubkey={sender} onAvatarClick={undefined} />
{isSenderAdmin ? <CrownIcon /> : null}
</StyledAvatar>
<StyledAuthorNamesContainer>
{!!profileName && <Name>{profileName}</Name>}
<Pubkey>{sender}</Pubkey>

@ -14,6 +14,7 @@ import {
useMessageHash,
useMessageReceivedAt,
useMessageSender,
useMessageSenderIsAdmin,
useMessageServerId,
useMessageServerTimestamp,
useMessageTimestamp,
@ -111,6 +112,7 @@ export const MessageInfo = ({ messageId, errors }: { messageId: string; errors:
const sentAt = useMessageTimestamp(messageId);
const serverTimestamp = useMessageServerTimestamp(messageId);
const receivedAt = useMessageReceivedAt(messageId);
const isSenderAdmin = useMessageSenderIsAdmin(messageId);
if (!messageId || !sender) {
return null;
@ -137,7 +139,7 @@ export const MessageInfo = ({ messageId, errors }: { messageId: string; errors:
<LabelWithInfo label={`${window.i18n('received')}:`} info={receivedAtStr} />
) : null}
<SpacerSM />
<MessageFrom sender={sender} />
<MessageFrom sender={sender} isSenderAdmin={isSenderAdmin} />
{hasError && (
<>
<SpacerSM />

@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { SnodeAPI } from '../../session/apis/snode_api/SNodeAPI';
import { ed25519Str } from '../../session/onions/onionPath';
import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/sync/syncUtils';
import { updateConfirmModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog';
import { SessionWrapperModal } from '../SessionWrapperModal';
@ -14,15 +14,27 @@ import { deleteAllLogs } from '../../node/logs';
import { clearInbox } from '../../session/apis/open_group_api/sogsv3/sogsV3ClearInbox';
import { getAllValidOpenGroupV2ConversationRoomInfos } from '../../session/apis/open_group_api/utils/OpenGroupUtils';
import { SessionRadioGroup } from '../basic/SessionRadioGroup';
import { ed25519Str } from '../../session/utils/String';
const deleteDbLocally = async () => {
window?.log?.info('last message sent successfully. Deleting everything');
window.persistStore?.purge();
window?.log?.info('store purged');
await deleteAllLogs();
window?.log?.info('deleteAllLogs: done');
await Data.removeAll();
window?.log?.info('Data.removeAll: done');
await Data.close();
window?.log?.info('Data.close: done');
await Data.removeDB();
window?.log?.info('Data.removeDB: done');
await Data.removeOtherData();
window?.log?.info('Data.removeOtherData: done');
window.localStorage.setItem('restart-reason', 'delete-account');
};

@ -11,6 +11,7 @@ import { SessionSpinner } from '../basic/SessionSpinner';
import { SpacerLG } from '../basic/Text';
import { SessionIconButton } from '../icon';
import { ProfileAvatar } from './EditProfileDialog';
import type { EditProfilePictureModalProps } from '../../types/ReduxTypes';
const StyledAvatarContainer = styled.div`
cursor: pointer;
@ -59,12 +60,6 @@ const uploadProfileAvatar = async (scaledAvatarUrl: string | null) => {
}
};
export type EditProfilePictureModalProps = {
avatarPath: string | null;
profileName: string | undefined;
ourId: string;
};
export const EditProfilePictureModal = (props: EditProfilePictureModalProps) => {
const dispatch = useDispatch();

@ -1,25 +1,26 @@
import { shell } from 'electron';
import React, { SessionDataTestId } from 'react';
import { ipcRenderer, shell } from 'electron';
import React, { useState, SessionDataTestId } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import useHover from 'react-use/lib/useHover';
import styled from 'styled-components';
import countryLookup from 'country-code-lookup';
import ip2country from 'ip2country';
import { isEmpty, isTypedArray } from 'lodash';
import { CityResponse, Reader } from 'maxmind';
import { useMount } from 'react-use';
import { Snode } from '../../data/data';
import { onionPathModal } from '../../state/ducks/modalDialog';
import {
getFirstOnionPath,
getFirstOnionPathLength,
getIsOnline,
getOnionPathsCount,
useFirstOnionPath,
useFirstOnionPathLength,
useIsOnline,
useOnionPathsCount,
} from '../../state/selectors/onions';
import { Flex } from '../basic/Flex';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionSpinner } from '../basic/SessionSpinner';
import { SessionIcon, SessionIconButton } from '../icon';
import { SessionWrapperModal } from '../SessionWrapperModal';
export type StatusLightType = {
glowStartDelay: number;
@ -76,11 +77,27 @@ const OnionCountryDisplay = ({ labelText, snodeIp }: { snodeIp?: string; labelTe
return hoverable;
};
let reader: Reader<CityResponse> | null;
const OnionPathModalInner = () => {
const onionPath = useSelector(getFirstOnionPath);
const isOnline = useSelector(getIsOnline);
// including the device and destination in calculation
const onionPath = useFirstOnionPath();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_dataLoaded, setDataLoaded] = useState(false);
const isOnline = useIsOnline();
const glowDuration = onionPath.length + 2;
useMount(() => {
ipcRenderer.once('load-maxmind-data-complete', (_event, content) => {
const asArrayBuffer = content as Uint8Array;
if (asArrayBuffer && isTypedArray(asArrayBuffer) && !isEmpty(asArrayBuffer)) {
reader = new Reader<CityResponse>(Buffer.from(asArrayBuffer.buffer));
setDataLoaded(true); // retrigger a rerender
}
});
ipcRenderer.send('load-maxmind-data');
});
if (!isOnline || !onionPath || onionPath.length === 0) {
return <SessionSpinner loading={true} />;
}
@ -104,7 +121,6 @@ const OnionPathModalInner = () => {
<Flex container={true}>
<StyledLightsContainer>
<StyledVerticalLine />
<Flex container={true} flexDirection="column" alignItems="center" height="100%">
{nodes.map((_snode: Snode | any, index: number) => {
return (
@ -119,19 +135,25 @@ const OnionPathModalInner = () => {
</StyledLightsContainer>
<Flex container={true} flexDirection="column" alignItems="flex-start">
{nodes.map((snode: Snode | any) => {
let labelText = snode.label
? snode.label
: countryLookup.byIso(ip2country(snode.ip))?.country;
if (!labelText) {
labelText = window.i18n('unknownCountry');
}
return labelText ? (
const country = reader?.get(snode.ip || '0.0.0.0')?.country;
const locale = (window.i18n as any).getLocale() as string;
// typescript complains that the [] operator cannot be used with the 'string' coming from getLocale()
const countryNamesAsAny = country?.names as any;
const countryName =
snode.label || // to take care of the "Device" case
countryNamesAsAny?.[locale] || // try to find the country name based on the user local first
// eslint-disable-next-line dot-notation
countryNamesAsAny?.['en'] || // if not found, fallback to the country in english
window.i18n('unknownCountry');
return (
<OnionCountryDisplay
labelText={labelText}
labelText={countryName}
snodeIp={snode.ip}
key={`country-${snode.ip}`}
/>
) : null;
);
})}
</Flex>
</Flex>
@ -192,9 +214,9 @@ export const ActionPanelOnionStatusLight = (props: {
}) => {
const { isSelected, handleClick, id } = props;
const onionPathsCount = useSelector(getOnionPathsCount);
const firstPathLength = useSelector(getFirstOnionPathLength);
const isOnline = useSelector(getIsOnline);
const onionPathsCount = useOnionPathsCount();
const firstPathLength = useFirstOnionPathLength();
const isOnline = useIsOnline();
// Set icon color based on result
const errorColor = 'var(--button-path-error-color)';

@ -1,6 +1,7 @@
import { shell } from 'electron';
import React, { Dispatch, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
import { useLastMessage } from '../../hooks/useParamSelector';
import { MessageInteraction } from '../../interactions';
@ -119,6 +120,14 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => {
}
};
useKey('Enter', () => {
void onClickOkHandler();
});
useKey('Escape', () => {
onClickCancelHandler();
});
useEffect(() => {
if (isLoading) {
if (conversationId && lastMessage?.interactionType) {

@ -11,8 +11,7 @@ import { matchesHash, validatePassword } from '../../util/passwordUtils';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SpacerSM } from '../basic/Text';
export type PasswordAction = 'set' | 'change' | 'remove' | 'enter';
import type { PasswordAction } from '../../types/ReduxTypes';
interface Props {
passwordAction: PasswordAction;

@ -63,6 +63,7 @@ const Password = (props: PasswordProps) => {
<input
type="password"
id="seed-input-password"
data-testid="password-input"
placeholder={i18n('enterPassword')}
onKeyUp={onEnter}
/>
@ -78,12 +79,14 @@ const Password = (props: PasswordProps) => {
text={i18n('done')}
buttonType={SessionButtonType.Simple}
onClick={confirmPassword}
dataTestId="session-confirm-ok-button"
/>
<SessionButton
text={i18n('cancel')}
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={onClose}
dataTestId="session-confirm-cancel-button"
/>
</div>
</>
@ -195,7 +198,6 @@ interface ModalInnerProps {
export const SessionSeedModal = (props: ModalInnerProps) => {
const { onClickOk } = props;
const [loadingPassword, setLoadingPassword] = useState(true);
const [loadingSeed, setLoadingSeed] = useState(true);
const [recoveryPhrase, setRecoveryPhrase] = useState('');
const [hasPassword, setHasPassword] = useState<null | boolean>(null);
@ -204,30 +206,22 @@ export const SessionSeedModal = (props: ModalInnerProps) => {
const dispatch = useDispatch();
useMount(() => {
async function checkHasPassword() {
if (!loadingPassword) {
async function validateAccess() {
if (passwordHash || recoveryPhrase) {
return;
}
const hash = await Data.getPasswordHash();
setHasPassword(!!hash);
setPasswordHash(hash || '');
setLoadingPassword(false);
}
async function getRecoveryPhrase() {
if (recoveryPhrase) {
return false;
}
const newRecoveryPhrase = getCurrentRecoveryPhrase();
setRecoveryPhrase(newRecoveryPhrase);
setLoadingSeed(false);
return true;
}
setTimeout(() => (document.getElementById('seed-input-password') as any)?.focus(), 100);
void checkHasPassword();
void getRecoveryPhrase();
void validateAccess();
});
const onClose = () => dispatch(recoveryPhraseModal(null));

@ -18,6 +18,7 @@ import { Avatar, AvatarSize } from '../avatar/Avatar';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SessionSpinner } from '../basic/SessionSpinner';
import { SpacerMD } from '../basic/Text';
import { Constants } from '../../session';
function GroupAvatar({
isPublic,
@ -103,6 +104,11 @@ export function UpdateGroupNameDialog(props: { conversationId: string }) {
return;
}
if (trimmedGroupName.length > Constants.VALIDATION.MAX_GROUP_NAME_LENGTH) {
onShowError(window.i18n('invalidGroupNameTooLong'));
return;
}
onShowError('');
if (trimmedGroupName !== originalGroupName || newAvatarObjecturl !== oldAvatarPath) {
@ -179,6 +185,7 @@ export function UpdateGroupNameDialog(props: { conversationId: string }) {
required={true}
aria-required={true}
autoFocus={true}
maxLength={Constants.VALIDATION.MAX_GROUP_NAME_LENGTH}
data-testid="group-name-input"
/>
) : null}

@ -88,7 +88,19 @@ export type SessionIconType =
export type SessionIconSize = 'tiny' | 'small' | 'medium' | 'large' | 'huge' | 'huge2' | 'max';
export const icons: Record<string, { path: string; viewBox: string; ratio: number }> = {
export type ClipRule = 'nonzero' | 'evenodd' | 'inherit';
export type FillRule = 'nonzero' | 'evenodd';
type IconProps = {
path: string;
viewBox: string;
ratio: number;
fill?: string;
clipRule?: ClipRule;
fillRule?: FillRule;
};
export const icons: Record<SessionIconType, IconProps> = {
addUser: {
path: 'M8.85,2.17c-1.73,0-3.12,1.4-3.12,3.12s1.4,3.12,3.12,3.12c1.73,0,3.13-1.4,3.13-3.12S10.58,2.17,8.85,2.17z M8.85,0.08c2.88,0,5.21,2.33,5.21,5.21s-2.33,5.21-5.21,5.21s-5.2-2.33-5.2-5.21C3.65,2.42,5.98,0.08,8.85,0.08z M20.83,5.29 c0.54,0,0.98,0.41,1.04,0.93l0.01,0.11v2.08h2.08c0.54,0,0.98,0.41,1.04,0.93v0.12c0,0.54-0.41,0.98-0.93,1.04l-0.11,0.01h-2.08 v2.08c0,0.58-0.47,1.04-1.04,1.04c-0.54,0-0.98-0.41-1.04-0.93l-0.01-0.11v-2.08h-2.08c-0.54,0-0.98-0.41-1.04-0.93l-0.01-0.11 c0-0.54,0.41-0.98,0.93-1.04l0.11-0.01h2.08V6.34C19.79,5.76,20.26,5.29,20.83,5.29z M12.5,12.58c2.8,0,5.09,2.21,5.2,4.99v0.22 v2.08c0,0.58-0.47,1.04-1.04,1.04c-0.54,0-0.98-0.41-1.04-0.93l-0.01-0.11v-2.08c0-1.67-1.3-3.03-2.95-3.12h-0.18H5.21 c-1.67,0-3.03,1.3-3.12,2.95v0.18v2.08c0,0.58-0.47,1.04-1.04,1.04c-0.54,0-0.98-0.41-1.04-0.93L0,19.88V17.8 c0-2.8,2.21-5.09,4.99-5.2h0.22h7.29V12.58z',
viewBox: '0 0 25 21',

@ -1,7 +1,8 @@
import React, { SessionDataTestId } from 'react';
import styled, { css, keyframes } from 'styled-components';
import React, { memo, SessionDataTestId } from 'react';
import styled, { css, CSSProperties, keyframes } from 'styled-components';
import { icons, SessionIconSize, SessionIconType } from '.';
import { ClipRule, FillRule } from './Icons';
export type SessionIconProps = {
iconType: SessionIconType;
@ -16,6 +17,8 @@ export type SessionIconProps = {
noScale?: boolean;
backgroundColor?: string;
dataTestId?: SessionDataTestId;
style?: CSSProperties;
unreadCount?: number;
};
const getIconDimensionFromIconSize = (iconSize: SessionIconSize | number) => {
@ -54,6 +57,9 @@ type StyledSvgProps = {
noScale?: boolean;
iconColor?: string;
backgroundColor?: string;
fill?: string;
clipRule?: ClipRule;
filleRule?: FillRule;
};
const rotate = keyframes`
@ -118,37 +124,28 @@ const animation = (props: {
return undefined;
};
const Svg = React.memo(styled.svg<StyledSvgProps>`
const Svg = memo(styled.svg<StyledSvgProps>`
width: ${props => props.width};
transform: ${props => `rotate(${props.iconRotation}deg)`};
${props => animation(props)};
border-radius: ${props => props.borderRadius};
background-color: ${props =>
props.backgroundColor ? props.backgroundColor : '--button-icon-background-color'};
border-radius: ${props => (props.borderRadius ? props.borderRadius : '')};
props.backgroundColor ? props.backgroundColor : 'var(--button-icon-background-color)'};
filter: ${props => (props.noScale ? `drop-shadow(0px 0px 4px ${props.iconColor})` : '')};
fill: ${props => (props.iconColor ? props.iconColor : '--button-icon-stroke-color')};
fill: ${props => (props.iconColor ? props.iconColor : 'var(--button-icon-stroke-color)')};
padding: ${props => (props.iconPadding ? props.iconPadding : '')};
transition: inherit;
`);
const SessionSvg = (props: {
viewBox: string;
path: string | Array<string>;
width: string | number;
height: string | number;
iconRotation: number;
iconColor?: string;
rotateDuration?: number;
glowDuration?: number;
glowStartDelay?: number;
noScale?: boolean;
borderRadius?: string;
backgroundColor?: string;
iconPadding?: string;
dataTestId?: SessionDataTestId;
}) => {
const colorSvg = props.iconColor ? props.iconColor : '--button-icon-stroke-color';
const SessionSvg = (
props: StyledSvgProps & {
viewBox: string;
path: string | Array<string>;
style?: CSSProperties;
dataTestId?: SessionDataTestId;
}
) => {
const colorSvg = props.iconColor ? props.iconColor : 'var(--button-icon-stroke-color)';
const pathArray = props.path instanceof Array ? props.path : [props.path];
const propsToPick = {
width: props.width,
@ -163,6 +160,10 @@ const SessionSvg = (props: {
backgroundColor: props.backgroundColor,
borderRadius: props.borderRadius,
iconPadding: props.iconPadding,
fill: props.fill,
clipRule: props.clipRule,
fillRule: props.filleRule,
style: props.style,
dataTestId: props.dataTestId,
};
@ -186,6 +187,7 @@ export const SessionIcon = (props: SessionIconProps) => {
noScale,
backgroundColor,
iconPadding,
style,
dataTestId,
} = props;
let { iconSize, iconRotation } = props;
@ -195,6 +197,9 @@ export const SessionIcon = (props: SessionIconProps) => {
const iconDimensions = getIconDimensionFromIconSize(iconSize);
const iconDef = icons[iconType];
const ratio = iconDef?.ratio || 1;
const fill = iconDef?.fill || undefined;
const clipRule = iconDef?.clipRule || 'nonzero';
const fillRule = iconDef?.fillRule || 'nonzero';
return (
<SessionSvg
@ -211,6 +216,10 @@ export const SessionIcon = (props: SessionIconProps) => {
iconColor={iconColor}
backgroundColor={backgroundColor}
iconPadding={iconPadding}
fill={fill}
clipRule={clipRule}
filleRule={fillRule}
style={style}
dataTestId={dataTestId}
/>
);

@ -2,13 +2,10 @@ import classNames from 'classnames';
import _ from 'lodash';
import React, { KeyboardEvent, SessionDataTestId } from 'react';
import styled from 'styled-components';
import { SessionIcon, SessionIconProps } from '.';
import { SessionNotificationCount } from './SessionNotificationCount';
import { SessionIcon, SessionIconProps } from './SessionIcon';
interface SProps extends SessionIconProps {
onClick?: (e?: React.MouseEvent<HTMLDivElement>) => void;
notificationCount?: number;
onClick?: (e?: React.MouseEvent<HTMLButtonElement>) => void;
isSelected?: boolean;
isHidden?: boolean;
margin?: string;
@ -17,9 +14,10 @@ interface SProps extends SessionIconProps {
id?: string;
style?: object;
tabIndex?: number;
children?: React.ReactNode;
}
const StyledSessionIconButton = styled.div<{ color?: string; isSelected?: boolean }>`
const StyledSessionIconButton = styled.button<{ color?: string; isSelected?: boolean }>`
background-color: var(--button-icon-background-color);
svg path {
@ -40,14 +38,13 @@ const StyledSessionIconButton = styled.div<{ color?: string; isSelected?: boolea
`;
// eslint-disable-next-line react/display-name
const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props, ref) => {
const SessionIconButtonInner = React.forwardRef<HTMLButtonElement, SProps>((props, ref) => {
const {
iconType,
iconSize,
iconColor,
iconRotation,
isSelected,
notificationCount,
glowDuration,
glowStartDelay,
noScale,
@ -61,14 +58,15 @@ const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props,
dataTestIdIcon,
style,
tabIndex,
children,
} = props;
const clickHandler = (e: React.MouseEvent<HTMLDivElement>) => {
const clickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
if (props.onClick) {
e.stopPropagation();
props.onClick(e);
}
};
const keyPressHandler = (e: KeyboardEvent<HTMLDivElement>) => {
const keyPressHandler = (e: KeyboardEvent<HTMLButtonElement>) => {
if (e.currentTarget.tabIndex > -1 && e.key === 'Enter' && props.onClick) {
e.stopPropagation();
props.onClick();
@ -80,7 +78,6 @@ const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props,
color={iconColor}
isSelected={isSelected}
className={classNames('session-icon-button', iconSize)}
role="button"
ref={ref}
id={id}
onClick={clickHandler}
@ -102,7 +99,7 @@ const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props,
iconPadding={iconPadding}
dataTestId={dataTestIdIcon}
/>
{Boolean(notificationCount) && <SessionNotificationCount count={notificationCount} />}
{children}
</StyledSessionIconButton>
);
});

@ -1,54 +1,87 @@
import React from 'react';
import styled from 'styled-components';
import { Constants } from '../../session';
type Props = {
overflowingAt: number;
centeredOnTop: boolean;
count?: number;
};
const StyledCountContainer = styled.div<{ centeredOnTop: boolean }>`
background: var(--unread-messages-alert-background-color);
color: var(--unread-messages-alert-text-color);
text-align: center;
padding: ${props => (props.centeredOnTop ? '1px 3px 0' : '1px 4px')};
const StyledCountContainer = styled.div<{ shouldRender: boolean }>`
position: absolute;
font-size: 18px;
line-height: 1.2;
top: 27px;
left: 28px;
padding: 1px 4px;
opacity: 1;
top: ${props => (props.centeredOnTop ? '-10px' : '27px')};
left: ${props => (props.centeredOnTop ? '50%' : '28px')};
font-size: var(--font-size-xs);
font-family: var(--font-default);
font-weight: 700;
height: 16px;
min-width: 16px;
line-height: 16px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-default);
border-radius: 58px;
font-weight: 700;
background: var(--unread-messages-alert-background-color);
flex-shrink: 0;
transition: var(--default-duration);
opacity: ${props => (props.shouldRender ? 1 : 0)};
text-align: center;
color: var(--unread-messages-alert-text-color);
transform: ${props => (props.centeredOnTop ? 'translateX(-50%)' : 'none')};
white-space: ${props => (props.centeredOnTop ? 'nowrap' : 'normal')};
`;
const StyledCount = styled.div`
const StyledCount = styled.div<{ centeredOnTop: boolean }>`
position: relative;
font-size: 0.6rem;
font-size: ${props => (props.centeredOnTop ? 'var(--font-size-xs)' : '0.6rem')};
`;
export const SessionNotificationCount = (props: Props) => {
const { count } = props;
const overflow = Boolean(count && count > 99);
const shouldRender = Boolean(count && count > 0);
if (overflow) {
return (
<StyledCountContainer shouldRender={shouldRender}>
<StyledCount>
{99}
<span>+</span>
</StyledCount>
</StyledCountContainer>
);
const OverflowingAt = (props: { overflowingAt: number }) => {
return (
<>
{props.overflowingAt}
<span>+</span>
</>
);
};
const NotificationOrUnreadCount = ({ centeredOnTop, overflowingAt, count }: Props) => {
if (!count) {
return null;
}
const overflowing = count > overflowingAt;
return (
<StyledCountContainer shouldRender={shouldRender}>
<StyledCount>{count}</StyledCount>
<StyledCountContainer centeredOnTop={centeredOnTop}>
<StyledCount centeredOnTop={centeredOnTop}>
{overflowing ? <OverflowingAt overflowingAt={overflowingAt} /> : count}
</StyledCount>
</StyledCountContainer>
);
};
export const SessionNotificationCount = (props: Pick<Props, 'count'>) => {
return (
<NotificationOrUnreadCount
centeredOnTop={false}
overflowingAt={Constants.CONVERSATION.MAX_GLOBAL_UNREAD_COUNT}
count={props.count}
/>
);
};
export const SessionUnreadCount = (props: Pick<Props, 'count'>) => {
return (
<NotificationOrUnreadCount
centeredOnTop={true}
overflowingAt={Constants.CONVERSATION.MAX_CONVO_UNREAD_COUNT}
count={props.count}
/>
);
};

@ -32,7 +32,7 @@ import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { UserUtils } from '../../session/utils';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { ActionPanelOnionStatusLight } from '../dialog/OnionStatusPathDialog';
import { SessionIconButton } from '../icon';
import { SessionIconButton } from '../icon/SessionIconButton';
import { LeftPaneSectionContainer } from './LeftPaneSectionContainer';
import { SettingsKey } from '../../data/settings-key';
@ -44,6 +44,7 @@ import { isDarkTheme } from '../../state/selectors/theme';
import { ensureThemeConsistency } from '../../themes/SessionTheme';
import { switchThemeTo } from '../../themes/switchTheme';
import { getOppositeTheme } from '../../util/theme';
import { SessionNotificationCount } from '../icon/SessionNotificationCount';
import { ReleasedFeatures } from '../../util/releaseFeature';
@ -102,10 +103,11 @@ const Section = (props: { type: SectionType }) => {
iconSize="medium"
dataTestId="message-section"
iconType={'chatBubble'}
notificationCount={unreadToShow}
onClick={handleClick}
isSelected={isSelected}
/>
>
{Boolean(unreadToShow) && <SessionNotificationCount count={unreadToShow} />}
</SessionIconButton>
);
case SectionType.Settings:
return (

@ -28,7 +28,7 @@ const StyledProgressBarContainer = styled.div`
const StyledProgressBarInner = styled.div`
background: var(--primary-color);
width: 90%;
transition: width 0.5s ease-in;
transition: width var(--default-duration) ease-in;
height: 100%;
`;
@ -61,6 +61,7 @@ const StyledBannerInner = styled.div`
.session-button {
margin-top: var(--margins-md);
flex-grow: 1;
}
`;
@ -75,11 +76,13 @@ const BannerInner = () => {
return (
<StyledBannerInner>
<p>{window.i18n('recoveryPhraseRevealMessage')}</p>
<SessionButton
text={window.i18n('recoveryPhraseRevealButtonText')}
onClick={showRecoveryPhraseModal}
dataTestId="reveal-recovery-phrase"
/>
<Flex container={true} alignItems="center" justifyContent="center">
<SessionButton
text={window.i18n('recoveryPhraseRevealButtonText')}
onClick={showRecoveryPhraseModal}
dataTestId="reveal-recovery-phrase"
/>
</Flex>
</StyledBannerInner>
);
};

@ -12,8 +12,8 @@ import {
} from '../../state/ducks/section';
import { getFocusedSettingsSection } from '../../state/selectors/section';
import { SessionIcon } from '../icon';
import { SessionSettingCategory } from '../settings/SessionSettings';
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
import type { SessionSettingCategory } from '../../types/ReduxTypes';
const StyledSettingsSectionTitle = styled.strong`
font-family: var(--font-accent), var(--font-default);
@ -42,7 +42,11 @@ const StyledSettingsListItem = styled.div<{ active: boolean }>`
}
`;
const getCategories = () => {
const getCategories = (): Array<{
id: SessionSettingCategory;
title: string;
dataTestId: SessionDataTestId;
}> => {
return [
{
id: 'privacy' as const,
@ -77,7 +81,7 @@ const getCategories = () => {
title: window.i18n('recoveryPhrase'),
},
{
id: 'ClearData' as const,
id: 'clearData' as const,
title: window.i18n('clearDataSettingsTitle'),
},
].map(m => ({ ...m, dataTestId: `${m.id}-settings-menu-item` as const }));
@ -95,7 +99,7 @@ const LeftPaneSettingsCategoryRow = (props: {
const dispatch = useDispatch();
const focusedSettingsSection = useSelector(getFocusedSettingsSection);
const isClearData = id === 'ClearData';
const isClearData = id === 'clearData';
return (
<StyledSettingsListItem
@ -113,7 +117,7 @@ const LeftPaneSettingsCategoryRow = (props: {
case 'recoveryPhrase':
dispatch(recoveryPhraseModal({}));
break;
case 'ClearData':
case 'clearData':
dispatch(updateDeleteAccountModal({}));
break;
default:

@ -4,10 +4,10 @@ import { createPortal } from 'react-dom';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { getUnreadConversationRequests } from '../../state/selectors/conversations';
import { isSearching } from '../../state/selectors/search';
import { getHideMessageRequestBanner } from '../../state/selectors/userConfig';
import { SessionIcon, SessionIconSize, SessionIconType } from '../icon';
import { MessageRequestBannerContextMenu } from '../menu/MessageRequestBannerContextMenu';
import { isSearching } from '../../state/selectors/search';
const StyledMessageRequestBanner = styled.div`
height: 64px;
@ -45,7 +45,8 @@ const StyledCircleIcon = styled.div`
const StyledUnreadCounter = styled.div`
font-weight: bold;
border-radius: var(--margins-sm);
background-color: var(--conversation-tab-bubble-background-color);
color: var(--unread-messages-alert-text-color);
background-color: var(--unread-messages-alert-background-color);
margin-left: var(--margins-sm);
min-width: 20px;
height: 20px;
@ -136,6 +137,6 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => {
);
};
const Portal = ({ children }: { children: any }) => {
const Portal = ({ children }: { children: React.ReactNode }) => {
return createPortal(children, document.querySelector('.inbox.index') as Element);
};

@ -10,6 +10,10 @@ import { Avatar, AvatarSize } from '../../avatar/Avatar';
import { openConversationWithMessages } from '../../../state/ducks/conversations';
import { updateUserDetailsModal } from '../../../state/ducks/modalDialog';
import {
ContextConversationProvider,
useConvoIdFromContext,
} from '../../../contexts/ConvoIdContext';
import {
useAvatarPath,
useConversationUsername,
@ -21,7 +25,6 @@ import {
import { isSearching } from '../../../state/selectors/search';
import { useSelectedConversationKey } from '../../../state/selectors/selectedConversation';
import { MemoConversationListItemContextMenu } from '../../menu/ConversationListItemContextMenu';
import { ContextConversationProvider, useConvoIdFromContext } from './ConvoIdContext';
import { ConversationListItemHeaderItem } from './HeaderItem';
import { MessageItem } from './MessageItem';
@ -31,7 +34,7 @@ type PropsHousekeeping = {
type Props = { conversationId: string } & PropsHousekeeping;
const Portal = ({ children }: { children: any }) => {
const Portal = ({ children }: { children: React.ReactNode }) => {
return createPortal(children, document.querySelector('.inbox.index') as Element);
};

@ -2,6 +2,7 @@ import classNames from 'classnames';
import React from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext';
import { Data } from '../../../data/data';
import {
useActiveAt,
@ -12,6 +13,7 @@ import {
useNotificationSetting,
useUnreadCount,
} from '../../../hooks/useParamSelector';
import { Constants } from '../../../session';
import {
openConversationToSpecificMessage,
openConversationWithMessages,
@ -20,7 +22,6 @@ import { isSearching } from '../../../state/selectors/search';
import { getIsMessageSection } from '../../../state/selectors/section';
import { Timestamp } from '../../conversation/Timestamp';
import { SessionIcon } from '../../icon';
import { useConvoIdFromContext } from './ConvoIdContext';
import { UserItem } from './UserItem';
const NotificationSettingIcon = () => {
@ -160,8 +161,14 @@ const UnreadCount = ({ convoId }: { convoId: string }) => {
const unreadMsgCount = useUnreadCount(convoId);
const forcedUnread = useIsForcedUnreadWithoutUnreadMsg(convoId);
const unreadWithOverflow =
unreadMsgCount > Constants.CONVERSATION.MAX_CONVO_UNREAD_COUNT
? `${Constants.CONVERSATION.MAX_CONVO_UNREAD_COUNT}+`
: unreadMsgCount || ' ';
// TODO would be good to merge the style of this with SessionNotificationCount or SessionUnreadCount at some point.
return unreadMsgCount > 0 || forcedUnread ? (
<p className="module-conversation-list-item__unread-count">{unreadMsgCount || ' '}</p>
<p className="module-conversation-list-item__unread-count">{unreadWithOverflow}</p>
) : null;
};

@ -2,6 +2,7 @@ import classNames from 'classnames';
import { isEmpty } from 'lodash';
import React from 'react';
import { useSelector } from 'react-redux';
import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext';
import {
useHasUnread,
useIsPrivate,
@ -15,7 +16,6 @@ import { assertUnreachable } from '../../../types/sqlSharedTypes';
import { TypingAnimation } from '../../conversation/TypingAnimation';
import { MessageBody } from '../../conversation/message/message-content/MessageBody';
import { SessionIcon } from '../../icon';
import { useConvoIdFromContext } from './ConvoIdContext';
import { InteractionItem } from './InteractionItem';
export const MessageItem = () => {
@ -74,13 +74,21 @@ function IconMessageStatus({ status }: { status: LastMessageStatusType }) {
const nonErrorIconColor = 'var(--text-secondary-color';
switch (status) {
case 'error':
return <SessionIcon iconColor={'var(--danger-color'} iconType="error" iconSize="tiny" />;
return (
<SessionIcon
iconColor={'var(--danger-color'}
iconType="error"
iconSize="tiny"
style={{ flexShrink: 0 }}
/>
);
case 'read':
return (
<SessionIcon
iconColor={nonErrorIconColor}
iconType="doubleCheckCircleFilled"
iconSize="tiny"
style={{ flexShrink: 0 }}
/>
);
case 'sending':
@ -90,10 +98,18 @@ function IconMessageStatus({ status }: { status: LastMessageStatusType }) {
iconColor={nonErrorIconColor}
iconType="sending"
iconSize="tiny"
style={{ flexShrink: 0 }}
/>
);
case 'sent':
return <SessionIcon iconColor={nonErrorIconColor} iconType="circleCheck" iconSize="tiny" />;
return (
<SessionIcon
iconColor={nonErrorIconColor}
iconType="circleCheck"
iconSize="tiny"
style={{ flexShrink: 0 }}
/>
);
case undefined:
return null;
default:

@ -1,6 +1,7 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { isEmpty } from 'lodash';
import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext';
import {
useConversationRealName,
useConversationUsername,
@ -10,7 +11,6 @@ import {
import { PubKey } from '../../../session/types';
import { isSearching } from '../../../state/selectors/search';
import { ContactName } from '../../conversation/ContactName';
import { useConvoIdFromContext } from './ConvoIdContext';
export const UserItem = () => {
const conversationId = useConvoIdFromContext();
@ -37,15 +37,13 @@ export const UserItem = () => {
}
return (
<div className="module-conversation__user">
<ContactName
pubkey={displayedPubkey}
name={username}
profileName={displayName}
module="module-conversation__user"
boldProfileName={true}
shouldShowPubkey={shouldShowPubkey}
/>
</div>
<ContactName
pubkey={displayedPubkey}
name={username}
profileName={displayName}
module="module-conversation__user"
boldProfileName={true}
shouldShowPubkey={shouldShowPubkey}
/>
);
};

@ -138,7 +138,7 @@ export const OverlayMessage = () => {
justifyContent="space-between"
alignItems="center"
width="100%"
padding="0 var(--margins-md)" // YourSessionIDSelectable already has a left margin of 15px
padding="0 var(--margins-sm)" // YourSessionIDSelectable already has a left margin of 15px
>
<YourSessionIDSelectable />
<SessionIconButton iconSize="small" iconType="copy" onClick={copyOurSessionID} />

@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
import { declineConversationWithoutConfirm } from '../../../interactions/conversationInteractions';
import { ed25519Str } from '../../../session/onions/onionPath';
import { forceSyncConfigurationNowIfNeeded } from '../../../session/utils/sync/syncUtils';
import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import { resetLeftOverlayMode } from '../../../state/ducks/section';
@ -13,6 +12,7 @@ import { useSelectedConversationKey } from '../../../state/selectors/selectedCon
import { SessionButton, SessionButtonColor } from '../../basic/SessionButton';
import { SpacerLG } from '../../basic/Text';
import { ConversationListItem } from '../conversation-list-item/ConversationListItem';
import { ed25519Str } from '../../../session/utils/String';
const MessageRequestListPlaceholder = styled.div`
color: var(--conversation-tab-text-color);

@ -2,7 +2,7 @@ import { isString } from 'lodash';
import React from 'react';
import { useSelector } from 'react-redux';
import { AutoSizer, Index, List, ListRowProps } from 'react-virtualized';
import styled from 'styled-components';
import styled, { CSSProperties } from 'styled-components';
import {
DirectContactsByNameType,
getDirectContactsByName,
@ -51,10 +51,10 @@ const renderRow = (props: ListRowProps) => {
}
if (isString(item)) {
return <ContactRowBreak style={style} key={key} char={item} />;
return <ContactRowBreak style={style as CSSProperties} key={key} char={item} />;
}
return <ContactRow style={style} key={key} {...item} />;
return <ContactRow style={style as CSSProperties} key={key} {...item} />;
};
const unknownSection = 'unknown';

@ -14,7 +14,7 @@ const StyledActionRow = styled.button`
display: flex;
align-items: center;
border-bottom: 1px var(--border-color) solid;
transition-duration: 0.25s;
transition-duration: var(--default-duration);
width: 100%;
&:first-child {

@ -2,6 +2,7 @@ import React from 'react';
import { Item, Menu } from 'react-contexify';
import { useSelector } from 'react-redux';
import { useConvoIdFromContext } from '../../contexts/ConvoIdContext';
import { useIsPinned, useIsPrivate, useIsPrivateAndFriend } from '../../hooks/useParamSelector';
import { ConvoHub } from '../../session/conversations';
import { isSearching } from '../../state/selectors/search';
@ -10,7 +11,6 @@ import {
getIsMessageSection,
} from '../../state/selectors/section';
import { SessionContextMenuContainer } from '../SessionContextMenuContainer';
import { useConvoIdFromContext } from '../leftpane/conversation-list-item/ConvoIdContext';
import {
AcceptMsgRequestMenuItem,
BanMenuItem,

@ -2,6 +2,7 @@ import React from 'react';
import { Item, Submenu } from 'react-contexify';
import { useDispatch, useSelector } from 'react-redux';
import { useConvoIdFromContext } from '../../contexts/ConvoIdContext';
import {
useAvatarPath,
useConversationUsername,
@ -60,7 +61,6 @@ import {
import { useSelectedConversationKey } from '../../state/selectors/selectedConversation';
import { LocalizerKeys } from '../../types/LocalizerKeys';
import { SessionButtonColor } from '../basic/SessionButton';
import { useConvoIdFromContext } from '../leftpane/conversation-list-item/ConvoIdContext';
/** Menu items standardized */

@ -1,6 +1,6 @@
import React from 'react';
export const AccentText: React.FC = () => (
export const AccentText = () => (
<div className="session-content-accent-text">
<div className="session-content-accent-text title">{window.i18n('beginYourSession')}</div>
</div>

@ -5,7 +5,6 @@ import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { ConvoHub } from '../../session/conversations';
import { mnDecode } from '../../session/crypto/mnemonic';
import { PromiseUtils, StringUtils, ToastUtils } from '../../session/utils';
import { TaskTimedOutError } from '../../session/utils/Promise';
import { fromHex } from '../../session/utils/String';
import { trigger } from '../../shims/events';
import {
@ -103,7 +102,10 @@ export async function signInWithRecovery(signInDetails: {
* This is will try to sign in with the user recovery phrase.
* If no libsession updates is received in 60seconds, the loading will be canceled.
*/
export async function signInWithLinking(signInDetails: { userRecoveryPhrase: string }) {
export async function signInWithLinking(
signInDetails: { userRecoveryPhrase: string },
setSignInMode: (phase: SignInMode) => void
) {
const { userRecoveryPhrase } = signInDetails;
window?.log?.info('LINKING DEVICE');
@ -135,18 +137,14 @@ export async function signInWithLinking(signInDetails: { userRecoveryPhrase: str
trigger('openInbox');
} catch (e) {
await resetRegistration();
if (e instanceof TaskTimedOutError) {
ToastUtils.pushToastError(
'registrationError',
'Could not find your display name. Please Sign In by Restoring Your Account instead.'
);
} else {
ToastUtils.pushToastError(
'registrationError',
`Error: ${e.message || 'Something went wrong'}`
);
}
window?.log?.warn('exception during registration:', e);
ToastUtils.pushToastError('registrationError', window.i18n('displayNameErrorNew'));
window?.log?.error(
'[signInWithLinking] Error during sign in by linking lets try and sign in by recovery phrase',
e.message || e
);
getSwarmPollingInstance().stop(e);
await setSignWithRecoveryPhrase(false);
setSignInMode(SignInMode.UsingRecoveryPhrase);
}
}

@ -123,7 +123,7 @@ export const SignInTab = () => {
// from the configuration message will be used.
const showDisplayNameField = isRecovery;
// Display name is required only on isRecoveryMode
// Display name is required only on isRecoveryMode or if linking a device fails
const displayNameOK = (isRecovery && !displayNameError && !!displayName) || isLinking;
// Seed is mandatory no matter which mode
@ -139,9 +139,12 @@ export const SignInTab = () => {
});
} else if (isLinking) {
setIsLoading(true);
await signInWithLinking({
userRecoveryPhrase: recoveryPhrase,
});
await signInWithLinking(
{
userRecoveryPhrase: recoveryPhrase,
},
setSignInMode
);
setIsLoading(false);
}
};

@ -1,7 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import useTimeoutFn from 'react-use/lib/useTimeoutFn';
import { isAutoLogin } from '../../shared/env_vars';
import { Noop } from '../../types/Util';
import { Flex } from '../basic/Flex';
import { SessionButton } from '../basic/SessionButton';
import { SessionIdEditable } from '../basic/SessionIdEditable';
@ -34,7 +33,7 @@ const ContinueSignUpButton = (props: { continueSignUp: () => void }) => {
return <SessionButton onClick={props.continueSignUp} text={window.i18n('continue')} />;
};
const SignUpDefault = (props: { createSessionID: Noop }) => {
const SignUpDefault = (props: { createSessionID: () => void }) => {
return (
<div className="session-registration__content">
<CreateSessionIdButton createSessionID={props.createSessionID} />
@ -58,7 +57,7 @@ export const GoBackMainMenuButton = () => {
);
};
const SignUpSessionIDShown = (props: { continueSignUp: Noop }) => {
const SignUpSessionIDShown = (props: { continueSignUp: () => void }) => {
return (
<div className="session-registration__content">
<Flex flexDirection="row" container={true} alignItems="center">

@ -58,6 +58,7 @@ const StyledResultText = styled.div`
display: inline-flex;
flex-direction: column;
align-items: stretch;
min-width: 0;
`;
const ResultsHeader = styled.div`

@ -1,8 +1,8 @@
import { isString } from 'lodash';
import React from 'react';
import styled, { CSSProperties } from 'styled-components';
import { useSelector } from 'react-redux';
import { AutoSizer, List } from 'react-virtualized';
import { isString } from 'lodash';
import styled, { CSSProperties } from 'styled-components';
import { ConversationListItem } from '../leftpane/conversation-list-item/ConversationListItem';
import { MessageSearchResult } from './MessageSearchResults';
@ -65,14 +65,14 @@ const VirtualizedList = () => {
return null;
}
if (isString(row)) {
return <SectionHeader title={row} style={style} key={key} />;
return <SectionHeader title={row} style={style as CSSProperties} key={key} />;
}
if (isContact(row)) {
return (
<ConversationListItem conversationId={row.contactConvoId} style={style} key={key} />
);
}
return <MessageSearchResult style={style} key={key} {...row} />;
return <MessageSearchResult style={style as CSSProperties} key={key} {...row} />;
}}
width={width}
autoHeight={false}

@ -20,13 +20,9 @@ const StyledButtonContainer = styled.div`
padding-inline-start: var(--margins-lg);
`;
export const SessionNotificationGroupSettings = (props: { hasPassword: boolean | null }) => {
export const SessionNotificationGroupSettings = () => {
const forceUpdate = useUpdate();
if (props.hasPassword === null) {
return null;
}
const initialNotificationEnabled =
window.getSettingValue(SettingsKey.settingsNotification) || NotificationType.message;

@ -1,7 +1,6 @@
import React from 'react';
import styled from 'styled-components';
import { Noop } from '../../types/Util';
import {
SessionButton,
SessionButtonColor,
@ -113,7 +112,7 @@ export const SessionSettingsItemWrapper = (props: {
);
};
export const SessionSettingsTitleWithLink = (props: { title: string; onClick: Noop }) => {
export const SessionSettingsTitleWithLink = (props: { title: string; onClick: () => void }) => {
const { onClick, title } = props;
return (
<StyledSettingItemClickable onClick={onClick}>

@ -1,8 +1,9 @@
import autoBind from 'auto-bind';
import { shell } from 'electron';
import React from 'react';
import React, { useState } from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import useMount from 'react-use/lib/useMount';
import { SettingsHeader } from './SessionSettingsHeader';
import { SessionIconButton } from '../icon';
@ -12,12 +13,12 @@ import { SessionNotificationGroupSettings } from './SessionNotificationGroupSett
import { Data } from '../../data/data';
import { sessionPassword } from '../../state/ducks/modalDialog';
import { SectionType, showLeftPaneSection } from '../../state/ducks/section';
import { PasswordAction } from '../dialog/SessionPasswordDialog';
import { SettingsCategoryAppearance } from './section/CategoryAppearance';
import { CategoryConversations } from './section/CategoryConversations';
import { SettingsCategoryHelp } from './section/CategoryHelp';
import { SettingsCategoryPermissions } from './section/CategoryPermissions';
import { SettingsCategoryPrivacy } from './section/CategoryPrivacy';
import type { SessionSettingCategory, PasswordAction } from '../../types/ReduxTypes';
export function displayPasswordModal(
passwordAction: PasswordAction,
@ -41,26 +42,10 @@ export function getCallMediaPermissionsSettings() {
return window.getSettingValue('call-media-permissions');
}
export type SessionSettingCategory =
| 'privacy'
| 'notifications'
| 'conversations'
| 'messageRequests'
| 'appearance'
| 'permissions'
| 'help'
| 'recoveryPhrase'
| 'ClearData';
export interface SettingsViewProps {
category: SessionSettingCategory;
}
interface State {
hasPassword: boolean | null;
shouldLockSettings: boolean | null;
}
const StyledVersionInfo = styled.div`
display: flex;
justify-content: space-between;
@ -109,33 +94,30 @@ const SessionInfo = () => {
const SettingInCategory = (props: {
category: SessionSettingCategory;
hasPassword: boolean;
onPasswordUpdated: (action: string) => void;
hasPassword: boolean;
}) => {
const { category, hasPassword, onPasswordUpdated } = props;
const { category, onPasswordUpdated, hasPassword } = props;
if (hasPassword === null) {
return null;
}
switch (category) {
// special case for blocked user
case 'conversations':
return <CategoryConversations />;
case 'appearance':
return <SettingsCategoryAppearance hasPassword={hasPassword} />;
return <SettingsCategoryAppearance />;
case 'notifications':
return <SessionNotificationGroupSettings hasPassword={hasPassword} />;
return <SessionNotificationGroupSettings />;
case 'privacy':
return (
<SettingsCategoryPrivacy onPasswordUpdated={onPasswordUpdated} hasPassword={hasPassword} />
);
case 'help':
return <SettingsCategoryHelp hasPassword={hasPassword} />;
return <SettingsCategoryHelp />;
case 'permissions':
return <SettingsCategoryPermissions hasPassword={hasPassword} />;
return <SettingsCategoryPermissions />;
// these three down there have no options, they are just a button
case 'ClearData':
case 'clearData':
case 'messageRequests':
case 'recoveryPhrase':
default:
@ -158,87 +140,48 @@ const StyledSettingsList = styled.div`
flex-direction: column;
`;
export class SessionSettingsView extends React.Component<SettingsViewProps, State> {
public settingsViewRef: React.RefObject<HTMLDivElement>;
public constructor(props: any) {
super(props);
this.state = {
hasPassword: null,
shouldLockSettings: true,
};
this.settingsViewRef = React.createRef();
autoBind(this);
export const SessionSettingsView = (props: SettingsViewProps) => {
const { category } = props;
const dispatch = useDispatch();
const [hasPassword, setHasPassword] = useState(true);
useMount(() => {
let isMounted = true;
// eslint-disable-next-line more/no-then
void Data.getPasswordHash().then(hash => {
this.setState({
hasPassword: !!hash,
});
if (isMounted) {
setHasPassword(!!hash);
}
});
}
public componentDidUpdate(_: SettingsViewProps, _prevState: State) {
const oldShouldRenderPasswordLock = _prevState.shouldLockSettings && _prevState.hasPassword;
const newShouldRenderPasswordLock = this.state.shouldLockSettings && this.state.hasPassword;
if (
newShouldRenderPasswordLock &&
newShouldRenderPasswordLock !== oldShouldRenderPasswordLock
) {
displayPasswordModal('enter', action => {
if (action === 'enter') {
// Unlocked settings
this.setState({
shouldLockSettings: false,
});
}
});
}
}
public render() {
const { category } = this.props;
const shouldRenderPasswordLock = this.state.shouldLockSettings && this.state.hasPassword;
return (
<div className="session-settings">
{shouldRenderPasswordLock ? (
<></>
) : (
<>
<SettingsHeader category={category} />
<StyledSettingsView>
<StyledSettingsList ref={this.settingsViewRef}>
<SettingInCategory
category={category}
onPasswordUpdated={this.onPasswordUpdated}
hasPassword={Boolean(this.state.hasPassword)}
/>
</StyledSettingsList>
<SessionInfo />
</StyledSettingsView>
</>
)}
</div>
);
}
return () => {
isMounted = false;
};
});
public onPasswordUpdated(action: string) {
function onPasswordUpdated(action: string) {
if (action === 'set' || action === 'change') {
this.setState({
hasPassword: true,
shouldLockSettings: true,
});
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Message));
setHasPassword(true);
dispatch(showLeftPaneSection(SectionType.Message));
}
if (action === 'remove') {
this.setState({
hasPassword: false,
});
setHasPassword(false);
}
}
}
return (
<div className="session-settings">
<SettingsHeader category={category} />
<StyledSettingsView>
<StyledSettingsList>
<SettingInCategory
category={category}
onPasswordUpdated={onPasswordUpdated}
hasPassword={hasPassword}
/>
</StyledSettingsList>
<SessionInfo />
</StyledSettingsView>
</div>
);
};

@ -44,7 +44,7 @@ export const SettingsHeader = (props: Props) => {
case 'privacy':
categoryTitle = window.i18n('privacySettingsTitle');
break;
case 'ClearData':
case 'clearData':
case 'messageRequests':
case 'recoveryPhrase':
throw new Error(`no header for should be tried to be rendered for "${category}"`);

@ -2,54 +2,51 @@ import React from 'react';
import useUpdate from 'react-use/lib/useUpdate';
import { SettingsKey } from '../../../data/settings-key';
import { isHideMenuBarSupported } from '../../../types/Settings';
import { useHasFollowSystemThemeEnabled } from '../../../state/selectors/settings';
import { ensureThemeConsistency } from '../../../themes/SessionTheme';
import { isHideMenuBarSupported } from '../../../types/Settings';
import { SessionToggleWithDescription } from '../SessionSettingListItem';
import { SettingsThemeSwitcher } from '../SettingsThemeSwitcher';
import { ZoomingSessionSlider } from '../ZoomingSessionSlider';
export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null }) => {
export const SettingsCategoryAppearance = () => {
const forceUpdate = useUpdate();
const isFollowSystemThemeEnabled = useHasFollowSystemThemeEnabled();
if (props.hasPassword !== null) {
const isHideMenuBarActive =
window.getSettingValue(SettingsKey.settingsMenuBar) === undefined
? true
: window.getSettingValue(SettingsKey.settingsMenuBar);
const isHideMenuBarActive =
window.getSettingValue(SettingsKey.settingsMenuBar) === undefined
? true
: window.getSettingValue(SettingsKey.settingsMenuBar);
return (
<>
<SettingsThemeSwitcher />
<ZoomingSessionSlider />
{isHideMenuBarSupported() && (
<SessionToggleWithDescription
onClickToggle={() => {
window.toggleMenuBar();
forceUpdate();
}}
title={window.i18n('hideMenuBarTitle')}
description={window.i18n('hideMenuBarDescription')}
active={isHideMenuBarActive}
/>
)}
return (
<>
<SettingsThemeSwitcher />
<ZoomingSessionSlider />
{isHideMenuBarSupported() && (
<SessionToggleWithDescription
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClickToggle={async () => {
const toggledValue = !isFollowSystemThemeEnabled;
await window.setSettingValue(SettingsKey.hasFollowSystemThemeEnabled, toggledValue);
if (!isFollowSystemThemeEnabled) {
await ensureThemeConsistency();
}
onClickToggle={() => {
window.toggleMenuBar();
forceUpdate();
}}
title={window.i18n('matchThemeSystemSettingTitle')}
description={window.i18n('matchThemeSystemSettingDescription')}
active={isFollowSystemThemeEnabled}
dataTestId="enable-follow-system-theme"
title={window.i18n('hideMenuBarTitle')}
description={window.i18n('hideMenuBarDescription')}
active={isHideMenuBarActive}
/>
</>
);
}
return null;
)}
<SessionToggleWithDescription
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClickToggle={async () => {
const toggledValue = !isFollowSystemThemeEnabled;
await window.setSettingValue(SettingsKey.hasFollowSystemThemeEnabled, toggledValue);
if (!isFollowSystemThemeEnabled) {
await ensureThemeConsistency();
}
}}
title={window.i18n('matchThemeSystemSettingTitle')}
description={window.i18n('matchThemeSystemSettingDescription')}
active={isFollowSystemThemeEnabled}
dataTestId="enable-follow-system-theme"
/>
</>
);
};

@ -4,38 +4,35 @@ import { SessionButtonShape, SessionButtonType } from '../../basic/SessionButton
import { SessionSettingButtonItem, SessionSettingsTitleWithLink } from '../SessionSettingListItem';
export const SettingsCategoryHelp = (props: { hasPassword: boolean | null }) => {
if (props.hasPassword !== null) {
return (
<>
<SessionSettingButtonItem
onClick={() => {
ipcRenderer.send('show-debug-log');
}}
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
buttonText={window.i18n('showDebugLog')}
title={window.i18n('reportIssue')}
description={window.i18n('shareBugDetails')}
/>
<SessionSettingsTitleWithLink
title={window.i18n('surveyTitle')}
onClick={() => void shell.openExternal('https://getsession.org/survey')}
/>
<SessionSettingsTitleWithLink
title={window.i18n('helpUsTranslateSession')}
onClick={() => void shell.openExternal('https://crowdin.com/project/session-desktop/')}
/>
<SessionSettingsTitleWithLink
title={window.i18n('faq')}
onClick={() => void shell.openExternal('https://getsession.org/faq')}
/>
<SessionSettingsTitleWithLink
title={window.i18n('support')}
onClick={() => void shell.openExternal('https://sessionapp.zendesk.com/hc/en-us')}
/>
</>
);
}
return null;
export const SettingsCategoryHelp = () => {
return (
<>
<SessionSettingButtonItem
onClick={() => {
ipcRenderer.send('show-debug-log');
}}
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
buttonText={window.i18n('showDebugLog')}
title={window.i18n('reportIssue')}
description={window.i18n('shareBugDetails')}
/>
<SessionSettingsTitleWithLink
title={window.i18n('surveyTitle')}
onClick={() => void shell.openExternal('https://getsession.org/survey')}
/>
<SessionSettingsTitleWithLink
title={window.i18n('helpUsTranslateSession')}
onClick={() => void shell.openExternal('https://crowdin.com/project/session-desktop/')}
/>
<SessionSettingsTitleWithLink
title={window.i18n('faq')}
onClick={() => void shell.openExternal('https://getsession.org/faq')}
/>
<SessionSettingsTitleWithLink
title={window.i18n('support')}
onClick={() => void shell.openExternal('https://sessionapp.zendesk.com/hc/en-us')}
/>
</>
);
};

@ -53,54 +53,51 @@ async function toggleStartInTray() {
}
}
export const SettingsCategoryPermissions = (props: { hasPassword: boolean | null }) => {
export const SettingsCategoryPermissions = () => {
const forceUpdate = useUpdate();
const isStartInTrayActive = Boolean(window.getSettingValue(SettingsKey.settingsStartInTray));
if (props.hasPassword !== null) {
return (
<>
<SessionToggleWithDescription
onClickToggle={async () => {
await window.toggleMediaPermissions();
forceUpdate();
}}
title={window.i18n('mediaPermissionsTitle')}
description={window.i18n('mediaPermissionsDescription')}
active={Boolean(window.getSettingValue('media-permissions'))}
dataTestId="enable-microphone"
/>
<SessionToggleWithDescription
onClickToggle={async () => {
await toggleCallMediaPermissions(forceUpdate);
forceUpdate();
}}
title={window.i18n('callMediaPermissionsTitle')}
description={window.i18n('callMediaPermissionsDescription')}
active={Boolean(window.getCallMediaPermissions())}
dataTestId="enable-calls"
/>
<SessionToggleWithDescription
onClickToggle={async () => {
const old = Boolean(window.getSettingValue(SettingsKey.settingsAutoUpdate));
await window.setSettingValue(SettingsKey.settingsAutoUpdate, !old);
forceUpdate();
}}
title={window.i18n('autoUpdateSettingTitle')}
description={window.i18n('autoUpdateSettingDescription')}
active={Boolean(window.getSettingValue(SettingsKey.settingsAutoUpdate))}
/>
<SessionToggleWithDescription
onClickToggle={async () => {
await toggleStartInTray();
forceUpdate();
}}
title={window.i18n('startInTrayTitle')}
description={window.i18n('startInTrayDescription')}
active={isStartInTrayActive}
/>
</>
);
}
return null;
return (
<>
<SessionToggleWithDescription
onClickToggle={async () => {
await window.toggleMediaPermissions();
forceUpdate();
}}
title={window.i18n('mediaPermissionsTitle')}
description={window.i18n('mediaPermissionsDescription')}
active={Boolean(window.getSettingValue('media-permissions'))}
dataTestId="enable-microphone"
/>
<SessionToggleWithDescription
onClickToggle={async () => {
await toggleCallMediaPermissions(forceUpdate);
forceUpdate();
}}
title={window.i18n('callMediaPermissionsTitle')}
description={window.i18n('callMediaPermissionsDescription')}
active={Boolean(window.getCallMediaPermissions())}
dataTestId="enable-calls"
/>
<SessionToggleWithDescription
onClickToggle={async () => {
const old = Boolean(window.getSettingValue(SettingsKey.settingsAutoUpdate));
await window.setSettingValue(SettingsKey.settingsAutoUpdate, !old);
forceUpdate();
}}
title={window.i18n('autoUpdateSettingTitle')}
description={window.i18n('autoUpdateSettingDescription')}
active={Boolean(window.getSettingValue(SettingsKey.settingsAutoUpdate))}
/>
<SessionToggleWithDescription
onClickToggle={async () => {
await toggleStartInTray();
forceUpdate();
}}
title={window.i18n('startInTrayTitle')}
description={window.i18n('startInTrayDescription')}
active={isStartInTrayActive}
/>
</>
);
};

@ -61,66 +61,66 @@ export const SettingsCategoryPrivacy = (props: {
const isLinkPreviewsOn = useHasLinkPreviewEnabled();
const areBlindedRequestsEnabled = useHasBlindedMsgRequestsEnabled();
if (props.hasPassword !== null) {
return (
<>
<SessionToggleWithDescription
onClickToggle={async () => {
const old = Boolean(window.getSettingValue(SettingsKey.settingsReadReceipt));
await window.setSettingValue(SettingsKey.settingsReadReceipt, !old);
forceUpdate();
}}
title={window.i18n('readReceiptSettingTitle')}
description={window.i18n('readReceiptSettingDescription')}
active={window.getSettingValue(SettingsKey.settingsReadReceipt)}
dataTestId="enable-read-receipts"
/>
<SessionToggleWithDescription
onClickToggle={async () => {
const old = Boolean(window.getSettingValue(SettingsKey.settingsTypingIndicator));
await window.setSettingValue(SettingsKey.settingsTypingIndicator, !old);
forceUpdate();
}}
title={window.i18n('typingIndicatorsSettingTitle')}
description={window.i18n('typingIndicatorsSettingDescription')}
active={Boolean(window.getSettingValue(SettingsKey.settingsTypingIndicator))}
childrenDescription={<TypingBubbleItem />}
/>
<SessionToggleWithDescription
onClickToggle={() => {
void toggleLinkPreviews(isLinkPreviewsOn, forceUpdate);
}}
title={window.i18n('linkPreviewsTitle')}
description={window.i18n('linkPreviewDescription')}
active={isLinkPreviewsOn}
/>
<SessionToggleWithDescription
onClickToggle={async () => {
const toggledValue = !areBlindedRequestsEnabled;
await window.setSettingValue(SettingsKey.hasBlindedMsgRequestsEnabled, toggledValue);
await SessionUtilUserProfile.insertUserProfileIntoWrapper(
UserUtils.getOurPubKeyStrFromCache()
);
await UserSync.queueNewJobIfNeeded();
forceUpdate();
return (
<>
<SessionToggleWithDescription
onClickToggle={async () => {
const old = Boolean(window.getSettingValue(SettingsKey.settingsReadReceipt));
await window.setSettingValue(SettingsKey.settingsReadReceipt, !old);
forceUpdate();
}}
title={window.i18n('readReceiptSettingTitle')}
description={window.i18n('readReceiptSettingDescription')}
active={window.getSettingValue(SettingsKey.settingsReadReceipt)}
dataTestId="enable-read-receipts"
/>
<SessionToggleWithDescription
onClickToggle={async () => {
const old = Boolean(window.getSettingValue(SettingsKey.settingsTypingIndicator));
await window.setSettingValue(SettingsKey.settingsTypingIndicator, !old);
forceUpdate();
}}
title={window.i18n('typingIndicatorsSettingTitle')}
description={window.i18n('typingIndicatorsSettingDescription')}
active={Boolean(window.getSettingValue(SettingsKey.settingsTypingIndicator))}
childrenDescription={<TypingBubbleItem />}
/>
<SessionToggleWithDescription
onClickToggle={() => {
void toggleLinkPreviews(isLinkPreviewsOn, forceUpdate);
}}
title={window.i18n('linkPreviewsTitle')}
description={window.i18n('linkPreviewDescription')}
active={isLinkPreviewsOn}
/>
<SessionToggleWithDescription
onClickToggle={async () => {
const toggledValue = !areBlindedRequestsEnabled;
await window.setSettingValue(SettingsKey.hasBlindedMsgRequestsEnabled, toggledValue);
await SessionUtilUserProfile.insertUserProfileIntoWrapper(
UserUtils.getOurPubKeyStrFromCache()
);
await UserSync.queueNewJobIfNeeded();
forceUpdate();
}}
title={window.i18n('blindedMsgReqsSettingTitle')}
description={window.i18n('blindedMsgReqsSettingDesc')}
active={areBlindedRequestsEnabled}
/>
{!props.hasPassword ? (
<SessionSettingButtonItem
title={window.i18n('setAccountPasswordTitle')}
description={window.i18n('setAccountPasswordDescription')}
onClick={() => {
displayPasswordModal('set', props.onPasswordUpdated);
}}
title={window.i18n('blindedMsgReqsSettingTitle')}
description={window.i18n('blindedMsgReqsSettingDesc')}
active={areBlindedRequestsEnabled}
buttonText={window.i18n('setPassword')}
dataTestId={'set-password-button'}
/>
{!props.hasPassword && (
<SessionSettingButtonItem
title={window.i18n('setAccountPasswordTitle')}
description={window.i18n('setAccountPasswordDescription')}
onClick={() => {
displayPasswordModal('set', props.onPasswordUpdated);
}}
buttonText={window.i18n('setPassword')}
dataTestId={'set-password-button'}
/>
)}
{props.hasPassword && (
) : (
<>
{/* We have a password, let's show the 'change' and 'remove' password buttons */}
<SessionSettingButtonItem
title={window.i18n('changeAccountPasswordTitle')}
description={window.i18n('changeAccountPasswordDescription')}
@ -130,8 +130,6 @@ export const SettingsCategoryPrivacy = (props: {
buttonText={window.i18n('changePassword')}
dataTestId="change-password-settings-button"
/>
)}
{props.hasPassword && (
<SessionSettingButtonItem
title={window.i18n('removeAccountPasswordTitle')}
description={window.i18n('removeAccountPasswordDescription')}
@ -142,9 +140,8 @@ export const SettingsCategoryPrivacy = (props: {
buttonText={window.i18n('removePassword')}
dataTestId="remove-password-settings-button"
/>
)}
</>
);
}
return null;
</>
)}
</>
);
};

@ -0,0 +1,16 @@
import { createContext, useContext } from 'react';
export type ScrollToLoadedReasons =
| 'quote-or-search-result'
| 'go-to-bottom'
| 'unread-indicator'
| 'load-more-top'
| 'load-more-bottom';
export const ScrollToLoadedMessageContext = createContext(
(_loadedMessageIdToScrollTo: string, _reason: ScrollToLoadedReasons) => {}
);
export function useScrollToLoadedMessage() {
return useContext(ScrollToLoadedMessageContext);
}

@ -0,0 +1,10 @@
import { createContext, useContext } from 'react';
/**
* When the message is rendered as part of the detailView (right panel) we disable onClick and make some other minor UI changes
*/
export const IsDetailMessageViewContext = createContext<boolean>(false);
export function useIsDetailMessageView() {
return useContext(IsDetailMessageViewContext);
}

@ -0,0 +1,7 @@
import { createContext, useContext } from 'react';
export const IsMessageVisibleContext = createContext(false);
export function useIsMessageVisible() {
return useContext(IsMessageVisibleContext);
}

@ -116,6 +116,10 @@ async function updateSwarmNodesForPubkey(
await channels.updateSwarmNodesForPubkey(pubkey, snodeEdKeys);
}
async function clearOutAllSnodesNotInPool(edKeysOfSnodePool: Array<string>): Promise<void> {
await channels.clearOutAllSnodesNotInPool(edKeysOfSnodePool);
}
// Closed group
/**
@ -843,6 +847,7 @@ export const Data = {
generateAttachmentKeyIfEmpty,
getSwarmNodesForPubkey,
updateSwarmNodesForPubkey,
clearOutAllSnodesNotInPool,
getAllEncryptionKeyPairsForGroup,
getLatestClosedGroupEncryptionKeyPair,
addClosedGroupEncryptionKeyPair,

@ -24,6 +24,7 @@ const channelsToMake = new Set([
'removeItemById',
'getSwarmNodesForPubkey',
'updateSwarmNodesForPubkey',
'clearOutAllSnodesNotInPool',
'saveConversation',
'fetchConvoMemoryDetails',
'getConversationById',

@ -9,7 +9,6 @@ import {
} from '../models/conversation';
import { ConversationTypeEnum } from '../models/conversationAttributes';
import { isUsAnySogsFromCache } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { CONVERSATION } from '../session/constants';
import { TimerOptions, TimerOptionsArray } from '../session/disappearing_messages/timerOptions';
import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils';
@ -329,12 +328,11 @@ export function useMessageReactsPropsById(messageId?: string) {
/**
* Returns the unread count of that conversation, or 0 if none are found.
* Note: returned value is capped at a max of CONVERSATION.MAX_UNREAD_COUNT
* Note: returned value is capped at a max of CONVERSATION.MAX_CONVO_UNREAD_COUNT
*/
export function useUnreadCount(conversationId?: string): number {
const convoProps = useConversationPropsById(conversationId);
const convoUnreadCount = convoProps?.unreadCount || 0;
return Math.min(CONVERSATION.MAX_UNREAD_COUNT, convoUnreadCount);
return convoProps?.unreadCount || 0;
}
export function useHasUnread(conversationId?: string): boolean {

@ -21,11 +21,10 @@ import { getSodiumRenderer } from '../session/crypto';
import { DecryptedAttachmentsManager } from '../session/crypto/DecryptedAttachmentsManager';
import { DisappearingMessageConversationModeType } from '../session/disappearing_messages/types';
import { GroupUpdateInfoChangeMessage } from '../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
import { ed25519Str } from '../session/onions/onionPath';
import { PubKey } from '../session/types';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { sleepFor } from '../session/utils/Promise';
import { fromHexToArray, toHex } from '../session/utils/String';
import { ed25519Str, fromHexToArray, toHex } from '../session/utils/String';
import { GroupSync } from '../session/utils/job_runners/jobs/GroupSyncJob';
import { UserSync } from '../session/utils/job_runners/jobs/UserSyncJob';
import { SessionUtilContact } from '../session/utils/libsession/libsession_utils_contacts';

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save