Merge remote-tracking branch 'oxen/unstable' into delete-and-backspace

pull/3061/head
Audric Ackermann 1 month ago
commit f7d574ce20

@ -1 +0,0 @@
--install.frozen-lockfile true

@ -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).

Binary file not shown.

@ -77,13 +77,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",
@ -96,12 +95,12 @@
"fs-extra": "9.0.0",
"glob": "7.1.2",
"image-type": "^4.1.0",
"ip2country": "1.0.1",
"libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.1/libsession_util_nodejs-v0.3.1.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 +139,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",
@ -167,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",
@ -177,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",
@ -217,10 +216,11 @@
"afterSign": "build/notarize.js",
"afterPack": "build/afterPackHook.js",
"artifactName": "${name}-${os}-${arch}-${version}.${ext}",
"extraResources": {
"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",

@ -210,7 +210,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;
@ -609,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);

@ -3,8 +3,8 @@ 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';
const SessionScrollButtonDiv = styled.div`
position: fixed;
@ -18,8 +18,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,6 +30,7 @@ export const SessionScrollButton = (props: { onClickScrollBottom: Noop }) => {
isHidden={!show}
onClick={props.onClickScrollBottom}
dataTestId="scroll-to-bottom-button"
unreadCount={unreadCount}
/>
</SessionScrollButtonDiv>
);

@ -1,12 +1,11 @@
import React from 'react';
import classNames from 'classnames';
import { CSSProperties } from 'styled-components';
import React from 'react';
import { Emojify } from './Emojify';
import {
useNicknameOrProfileNameOrShortenedPubkey,
useIsPrivate,
useNicknameOrProfileNameOrShortenedPubkey,
} from '../../hooks/useParamSelector';
import { Emojify } from './Emojify';
type Props = {
pubkey: string;
@ -25,12 +24,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');
@ -39,15 +46,19 @@ export const ContactName = (props: Props) => {
className={classNames(prefix, compact && 'compact')}
dir="auto"
data-testid={`${prefix}__profile-name`}
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;

@ -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,
@ -31,17 +35,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>;

@ -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();
@ -137,13 +137,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>
);
};

@ -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()
@ -76,13 +77,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) =>
@ -91,9 +92,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);
@ -154,7 +155,7 @@ export const MessageContent = (props: Props) => {
const toolTipTitle = moment(serverTimestamp || timestamp).format('llll');
const isDetailViewAndSupportsAttachmentCarousel =
props.isDetailView && canDisplayImage(attachments);
isDetailView && canDisplayImagePreview(attachments);
return (
<StyledMessageContent
@ -164,14 +165,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}`}
@ -184,6 +182,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, { 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: string;
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>

@ -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, { ReactElement, useEffect, useState } from 'react';
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';
@ -65,7 +66,7 @@ const StyledReadLess = styled.span`
type ReactionsProps = Omit<ReactionProps, 'emoji'>;
const Reactions = (props: ReactionsProps): ReactElement => {
const Reactions = (props: ReactionsProps) => {
const { messageId, reactions, inModal } = props;
return (
<StyledMessageReactions
@ -85,7 +86,7 @@ interface ExpandReactionsProps extends ReactionsProps {
handleExpand: () => void;
}
const CompressedReactions = (props: ExpandReactionsProps): ReactElement => {
const CompressedReactions = (props: ExpandReactionsProps) => {
const { messageId, reactions, inModal, handleExpand } = props;
return (
<StyledMessageReactions
@ -119,7 +120,7 @@ const CompressedReactions = (props: ExpandReactionsProps): ReactElement => {
);
};
const ExpandedReactions = (props: ExpandReactionsProps): ReactElement => {
const ExpandedReactions = (props: ExpandReactionsProps) => {
const { handleExpand } = props;
return (
<Flex container={true} flexDirection={'column'} alignItems={'center'} margin="4px 0 0">
@ -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,6 +5,7 @@ import styled from 'styled-components';
import { useMessageExpirationPropsById } from '../../../../hooks/useParamSelector';
import { useMessageStatus } from '../../../../state/selectors';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { getMostRecentMessageId } from '../../../../state/selectors/conversations';
import { useSelectedIsGroupOrCommunity } from '../../../../state/selectors/selectedConversation';
import { SpacerXS } from '../../../basic/Text';
@ -12,7 +13,6 @@ import { SessionIcon, SessionIconType } from '../../../icon';
import { ExpireTimer } from '../../ExpireTimer';
type Props = {
isDetailView: boolean;
messageId: string;
dataTestId?: string | undefined;
};
@ -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);

@ -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;
@ -135,7 +136,7 @@ export const ExpirableReadableMessage = (props: ExpirableReadableMessageProps) =
} = selected;
// NOTE we want messages on the left in the message detail view regardless of direction
const direction = props.isDetailView ? 'incoming' : _direction;
const direction = isDetailView ? 'incoming' : _direction;
const isIncoming = direction === 'incoming';
return (

@ -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 { getConversationController } 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>
);

@ -1,14 +1,8 @@
import { debounce, noop } from 'lodash';
import React, {
AriaRole,
MouseEventHandler,
useCallback,
useContext,
useLayoutEffect,
useState,
} from 'react';
import React, { AriaRole, MouseEventHandler, useCallback, 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 { getConversationController } from '../../../../session/conversations';
@ -28,7 +22,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 +88,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

@ -1,4 +1,4 @@
import React, { ReactElement, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { useMouse } from 'react-use';
import styled from 'styled-components';
import { useRightOverlayMode } from '../../../../hooks/useUI';
@ -62,7 +62,7 @@ export type ReactionProps = {
handlePopupClick?: () => void;
};
export const Reaction = (props: ReactionProps): ReactElement => {
export const Reaction = (props: ReactionProps) => {
const {
emoji,
messageId,

@ -1,11 +1,11 @@
import React, { ReactElement, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { Data } from '../../../../data/data';
import { findAndFormatContact } from '../../../../models/message';
import { PubKey } from '../../../../session/types/PubKey';
import { isDarkTheme } from '../../../../state/selectors/theme';
import { nativeEmojiData } from '../../../../util/emoji';
import { findAndFormatContact } from '../../../../models/message';
export type TipPosition = 'center' | 'left' | 'right';
@ -142,7 +142,7 @@ type Props = {
onClick: (...args: Array<any>) => void;
};
export const ReactionPopup = (props: Props): ReactElement => {
export const ReactionPopup = (props: Props) => {
const { messageId, emoji, count, senders, tooltipPosition = 'center', onClick } = props;
const [contacts, setContacts] = useState<Array<string>>([]);

@ -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,6 +1,6 @@
import { useDispatch } from 'react-redux';
// eslint-disable-next-line import/no-named-default
import { ChangeEvent, MouseEvent, default as React, ReactElement, useState } from 'react';
import { ChangeEvent, MouseEvent, default as React, useState } from 'react';
import { QRCode } from 'react-qr-svg';
import styled from 'styled-components';
import { Avatar, AvatarSize } from '../avatar/Avatar';
@ -69,7 +69,7 @@ type ProfileAvatarProps = {
ourId: string;
};
export const ProfileAvatar = (props: ProfileAvatarProps): ReactElement => {
export const ProfileAvatar = (props: ProfileAvatarProps) => {
const { newAvatarObjectUrl, avatarPath, profileName, ourId } = props;
return (
<Avatar
@ -86,7 +86,7 @@ type ProfileHeaderProps = ProfileAvatarProps & {
setMode: (mode: ProfileDialogModes) => void;
};
const ProfileHeader = (props: ProfileHeaderProps): ReactElement => {
const ProfileHeader = (props: ProfileHeaderProps) => {
const { avatarPath, profileName, ourId, onClick, setMode } = props;
return (
@ -114,7 +114,8 @@ const ProfileHeader = (props: ProfileHeaderProps): ReactElement => {
};
type ProfileDialogModes = 'default' | 'edit' | 'qr';
export const EditProfileDialog = (): ReactElement => {
export const EditProfileDialog = () => {
const dispatch = useDispatch();
const _profileName = useOurConversationUsername() || '';

@ -1,25 +1,26 @@
import { shell } from 'electron';
import React from 'react';
import { ipcRenderer, shell } from 'electron';
import React, { useState } 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,14 +1,14 @@
import React, { ReactElement, useState } from 'react';
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { useMessageReactsPropsById } from '../../hooks/useParamSelector';
import { clearSogsReactionByServerId } from '../../session/apis/open_group_api/sogsv3/sogsV3ClearReaction';
import { getConversationController } from '../../session/conversations';
import { updateReactClearAllModal } from '../../state/ducks/modalDialog';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { Flex } from '../basic/Flex';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SessionSpinner } from '../basic/SessionSpinner';
import { SessionWrapperModal } from '../SessionWrapperModal';
type Props = {
reaction: string;
@ -46,7 +46,7 @@ const StyledReactClearAllContainer = styled(Flex)`
}
`;
export const ReactClearAllModal = (props: Props): ReactElement => {
export const ReactClearAllModal = (props: Props) => {
const { reaction, messageId } = props;
const [clearingInProgress, setClearingInProgress] = useState(false);

@ -1,9 +1,10 @@
import { isEmpty, isEqual } from 'lodash';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { Data } from '../../data/data';
import { useMessageReactsPropsById } from '../../hooks/useParamSelector';
import { findAndFormatContact } from '../../models/message';
import { isUsAnySogsFromCache } from '../../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { UserUtils } from '../../session/utils';
import {
@ -18,6 +19,7 @@ import {
import { SortedReactionList } from '../../types/Reaction';
import { nativeEmojiData } from '../../util/emoji';
import { Reactions } from '../../util/reactions';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { Flex } from '../basic/Flex';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
@ -25,8 +27,6 @@ import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer';
import { ContactName } from '../conversation/ContactName';
import { MessageReactions } from '../conversation/message/message-content/MessageReactions';
import { SessionIconButton } from '../icon';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { findAndFormatContact } from '../../models/message';
const StyledReactListContainer = styled(Flex)`
width: 376px;
@ -218,7 +218,7 @@ const handleSenders = (senders: Array<string>, me: string) => {
return updatedSenders;
};
export const ReactListModal = (props: Props): ReactElement => {
export const ReactListModal = (props: Props) => {
const { reaction, messageId } = props;
const dispatch = useDispatch();

@ -16,6 +16,7 @@ export type SessionIconProps = {
noScale?: boolean;
backgroundColor?: string;
dataTestId?: string;
unreadCount?: number;
};
const getIconDimensionFromIconSize = (iconSize: SessionIconSize | number) => {

@ -2,9 +2,8 @@ import classNames from 'classnames';
import _ from 'lodash';
import React, { KeyboardEvent } from 'react';
import styled from 'styled-components';
import { SessionIcon, SessionIconProps } from '.';
import { SessionNotificationCount } from './SessionNotificationCount';
import { SessionNotificationCount, SessionUnreadCount } from './SessionNotificationCount';
interface SProps extends SessionIconProps {
onClick?: (e?: React.MouseEvent<HTMLButtonElement>) => void;
@ -61,6 +60,7 @@ const SessionIconButtonInner = React.forwardRef<HTMLButtonElement, SProps>((prop
dataTestIdIcon,
style,
tabIndex,
unreadCount,
} = props;
const clickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
if (props.onClick) {
@ -102,6 +102,7 @@ const SessionIconButtonInner = React.forwardRef<HTMLButtonElement, SProps>((prop
dataTestId={dataTestIdIcon}
/>
{Boolean(notificationCount) && <SessionNotificationCount count={notificationCount} />}
{Boolean(unreadCount) && <SessionUnreadCount count={unreadCount} />}
</StyledSessionIconButton>
);
});

@ -1,18 +1,20 @@
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<{ shouldRender: boolean }>`
const StyledCountContainer = styled.div<{ centeredOnTop: 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')};
transform: ${props => (props.centeredOnTop ? 'translateX(-50%)' : 'none')};
padding: ${props => (props.centeredOnTop ? '3px 3px' : '1px 4px')};
display: flex;
align-items: center;
justify-content: center;
@ -21,34 +23,56 @@ const StyledCountContainer = styled.div<{ shouldRender: boolean }>`
font-weight: 700;
background: var(--unread-messages-alert-background-color);
transition: var(--default-duration);
opacity: ${props => (props.shouldRender ? 1 : 0)};
text-align: center;
color: var(--unread-messages-alert-text-color);
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);
const OverflowingAt = (props: { overflowingAt: number }) => {
return (
<>
{props.overflowingAt}
<span>+</span>
</>
);
};
if (overflow) {
return (
<StyledCountContainer shouldRender={shouldRender}>
<StyledCount>
{99}
<span>+</span>
</StyledCount>
</StyledCountContainer>
);
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}
/>
);
};

@ -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';

@ -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 {
useMentionedUs,
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 = () => {

@ -1,5 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext';
import {
useConversationRealName,
useConversationUsername,
@ -9,7 +10,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();
@ -36,15 +36,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}
/>
);
};

@ -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';

@ -2,10 +2,11 @@ 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 { getConversationController } from '../../session/conversations';
import { isSearching } from '../../state/selectors/search';
import { getIsMessageSection } from '../../state/selectors/section';
import { useConvoIdFromContext } from '../leftpane/conversation-list-item/ConvoIdContext';
import { SessionContextMenuContainer } from '../SessionContextMenuContainer';
import {
AcceptMsgRequestMenuItem,
@ -17,16 +18,15 @@ import {
DeclineAndBlockMsgRequestMenuItem,
DeclineMsgRequestMenuItem,
DeleteMessagesMenuItem,
DeletePrivateConversationMenuItem,
InviteContactMenuItem,
LeaveGroupOrCommunityMenuItem,
MarkAllReadMenuItem,
MarkConversationUnreadMenuItem,
NotificationForConvoMenuItem,
ShowUserDetailsMenuItem,
UnbanMenuItem,
DeletePrivateConversationMenuItem,
NotificationForConvoMenuItem,
} from './Menu';
import { isSearching } from '../../state/selectors/search';
export type PropsContextConversationItem = {
triggerId: string;

@ -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,
@ -56,7 +57,6 @@ import { getIsMessageSection } from '../../state/selectors/section';
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>

@ -1,15 +1,15 @@
import React from 'react';
import styled, { CSSProperties } from 'styled-components';
import { useConversationUsername, useIsPrivate } from '../../hooks/useParamSelector';
import { MessageAttributes } from '../../models/messageType';
import { UserUtils } from '../../session/utils';
import { getOurPubKeyStrFromCache } from '../../session/utils/User';
import { openConversationToSpecificMessage } from '../../state/ducks/conversations';
import { ContactName } from '../conversation/ContactName';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { Timestamp } from '../conversation/Timestamp';
import { MessageBodyHighlight } from '../basic/MessageBodyHighlight';
import { MessageAttributes } from '../../models/messageType';
import { useConversationUsername, useIsPrivate } from '../../hooks/useParamSelector';
import { UserUtils } from '../../session/utils';
import { ContactName } from '../conversation/ContactName';
import { Timestamp } from '../conversation/Timestamp';
export type MessageResultProps = MessageAttributes & { snippet: string };
@ -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}

@ -25,13 +25,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) || NOTIFICATION.MESSAGE;

@ -1,23 +1,24 @@
import { shell } from 'electron';
import React from 'react';
import autoBind from 'auto-bind';
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';
import { SessionNotificationGroupSettings } from './SessionNotificationGroupSettings';
import { CategoryConversations } from './section/CategoryConversations';
import { SettingsCategoryPrivacy } from './section/CategoryPrivacy';
import { SettingsCategoryAppearance } from './section/CategoryAppearance';
import { Data } from '../../data/data';
import { SettingsCategoryPermissions } from './section/CategoryPermissions';
import { SettingsCategoryHelp } from './section/CategoryHelp';
import { sessionPassword } from '../../state/ducks/modalDialog';
import { PasswordAction } from '../dialog/SessionPasswordDialog';
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';
export function displayPasswordModal(
passwordAction: PasswordAction,
@ -57,11 +58,6 @@ export interface SettingsViewProps {
category: SessionSettingCategory;
}
interface State {
hasPassword: boolean | null;
shouldLockSettings: boolean | null;
}
const StyledVersionInfo = styled.div`
display: flex;
justify-content: space-between;
@ -110,30 +106,27 @@ 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 SessionSettingCategory.Conversations:
return <CategoryConversations />;
case SessionSettingCategory.Appearance:
return <SettingsCategoryAppearance hasPassword={hasPassword} />;
return <SettingsCategoryAppearance />;
case SessionSettingCategory.Notifications:
return <SessionNotificationGroupSettings hasPassword={hasPassword} />;
return <SessionNotificationGroupSettings />;
case SessionSettingCategory.Privacy:
return (
<SettingsCategoryPrivacy onPasswordUpdated={onPasswordUpdated} hasPassword={hasPassword} />
);
case SessionSettingCategory.Help:
return <SettingsCategoryHelp hasPassword={hasPassword} />;
return <SettingsCategoryHelp />;
case SessionSettingCategory.Permissions:
return <SettingsCategoryPermissions hasPassword={hasPassword} />;
return <SettingsCategoryPermissions />;
// these three down there have no options, they are just a button
case SessionSettingCategory.ClearData:
@ -159,87 +152,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>
);
};

@ -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 ConfigurationSync.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 ConfigurationSync.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);
}

@ -7,7 +7,6 @@ import {
hasValidOutgoingRequestValues,
} from '../models/conversation';
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';
@ -241,12 +240,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 {

@ -10,6 +10,7 @@ import {
dialog,
protocol as electronProtocol,
ipcMain as ipc,
IpcMainEvent,
Menu,
nativeTheme,
screen,
@ -154,7 +155,7 @@ if (windowFromUserConfig) {
ephemeralConfig.set('window', windowConfig);
}
// import {load as loadLocale} from '../..'
import { readFile } from 'fs-extra';
import { getAppRootPath } from '../node/getRootPath';
import { setLastestRelease } from '../node/latest_desktop_release';
import { load as loadLocale, LocaleMessagesWithNameType } from '../node/locale';
@ -1074,6 +1075,18 @@ ipc.on('close-debug-log', () => {
}
});
ipc.on('save-debug-log', saveDebugLog);
ipc.on('load-maxmind-data', async (event: IpcMainEvent) => {
try {
const appRoot =
app.isPackaged && process.resourcesPath ? process.resourcesPath : app.getAppPath();
const fileToRead = path.join(appRoot, 'mmdb', 'GeoLite2-Country.mmdb');
console.info(`loading maxmind data from file:"${fileToRead}"`);
const buffer = await readFile(fileToRead);
event.reply('load-maxmind-data-complete', new Uint8Array(buffer.buffer));
} catch (e) {
event.reply('load-maxmind-data-complete', null);
}
});
// This should be called with an ipc sendSync
ipc.on('get-media-permissions', event => {

@ -51,8 +51,9 @@ export const CONVERSATION = {
// Maximum voice message duraton of 5 minutes
// which equates to 1.97 MB
MAX_VOICE_MESSAGE_DURATION: 300,
MAX_UNREAD_COUNT: 999,
};
MAX_CONVO_UNREAD_COUNT: 999,
MAX_GLOBAL_UNREAD_COUNT: 99, // the global one does not look good with 4 digits (999+) so we have a smaller one for it
} as const;
/**
* The file server and onion request max upload size is 10MB precisely.

@ -3,6 +3,9 @@ import _ from 'lodash';
import { Attachment } from '../../types/Attachment';
import { encryptAttachment } from '../../util/crypto/attachmentsEncrypter';
import { uploadFileToFsWithOnionV4 } from '../apis/file_server_api/FileServerApi';
import { addAttachmentPadding } from '../crypto/BufferPadding';
import {
AttachmentPointer,
AttachmentPointerWithUrl,
@ -10,9 +13,6 @@ import {
Quote,
QuotedAttachmentWithUrl,
} from '../messages/outgoing/visibleMessage/VisibleMessage';
import { addAttachmentPadding } from '../crypto/BufferPadding';
import { encryptAttachment } from '../../util/crypto/attachmentsEncrypter';
import { uploadFileToFsWithOnionV4 } from '../apis/file_server_api/FileServerApi';
interface UploadParams {
attachment: Attachment;
@ -107,7 +107,9 @@ export async function uploadLinkPreviewToFileServer(
): Promise<PreviewWithAttachmentUrl | undefined> {
// some links do not have an image associated, and it makes the whole message fail to send
if (!preview?.image) {
window.log.warn('tried to upload file to FileServer without image.. skipping');
if (!preview) {
window.log.warn('tried to upload file to FileServer without image.. skipping');
}
return preview as any;
}
const image = await uploadToFileServer({

@ -336,7 +336,6 @@ const _getGlobalUnreadCount = (sortedConversations: Array<ReduxConversationType>
}
if (
globalUnreadCount < 100 &&
isNumber(conversation.unreadCount) &&
isFinite(conversation.unreadCount) &&
conversation.unreadCount > 0 &&
@ -345,7 +344,6 @@ const _getGlobalUnreadCount = (sortedConversations: Array<ReduxConversationType>
globalUnreadCount += conversation.unreadCount;
}
}
return globalUnreadCount;
};

@ -160,10 +160,10 @@ export const useMessageText = (messageId: string | undefined): string | undefine
return useMessagePropsByMessageId(messageId)?.propsForMessage.text;
};
export function useHideAvatarInMsgList(messageId?: string) {
export function useHideAvatarInMsgList(messageId?: string, isDetailView?: boolean) {
const msgProps = useMessagePropsByMessageId(messageId);
const selectedIsPrivate = useSelectedIsPrivate();
return msgProps?.propsForMessage.direction === 'outgoing' || selectedIsPrivate;
return isDetailView || msgProps?.propsForMessage.direction === 'outgoing' || selectedIsPrivate;
}
export function useMessageSelected(messageId?: string) {

@ -1,27 +1,41 @@
import { createSelector } from '@reduxjs/toolkit';
import { StateType } from '../reducer';
import { useSelector } from 'react-redux';
import { OnionState } from '../ducks/onion';
import { SectionType } from '../ducks/section';
import { StateType } from '../reducer';
export const getOnionPaths = (state: StateType): OnionState => state.onionPaths;
const getOnionPaths = (state: StateType): OnionState => state.onionPaths;
export const getOnionPathsCount = createSelector(
const getOnionPathsCount = createSelector(
getOnionPaths,
(state: OnionState): SectionType => state.snodePaths.length
);
export const getFirstOnionPath = createSelector(
const getFirstOnionPath = createSelector(
getOnionPaths,
(state: OnionState): Array<{ ip: string }> => state.snodePaths?.[0] || []
);
export const getFirstOnionPathLength = createSelector(
const getFirstOnionPathLength = createSelector(
getFirstOnionPath,
(state: Array<{ ip: string }>): number => state.length || 0
);
export const getIsOnline = createSelector(
getOnionPaths,
(state: OnionState): boolean => state.isOnline
);
const getIsOnline = createSelector(getOnionPaths, (state: OnionState): boolean => state.isOnline);
export const useOnionPathsCount = () => {
return useSelector(getOnionPathsCount);
};
export const useIsOnline = () => {
return useSelector(getIsOnline);
};
export const useFirstOnionPathLength = () => {
return useSelector(getFirstOnionPathLength);
};
export const useFirstOnionPath = () => {
return useSelector(getFirstOnionPath);
};

@ -1,5 +1,6 @@
import { isString } from 'lodash';
import { useSelector } from 'react-redux';
import { useUnreadCount } from '../../hooks/useParamSelector';
import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes';
import {
DisappearingMessageConversationModeType,
@ -302,6 +303,11 @@ export function useSelectedIsActive() {
return useSelector(getIsSelectedActive);
}
export function useSelectedUnreadCount() {
const selectedConversation = useSelectedConversationKey();
return useUnreadCount(selectedConversation);
}
export function useSelectedIsNoteToSelf() {
return useSelector(getIsSelectedNoteToSelf);
}

@ -104,9 +104,11 @@ export function isAudio(attachments?: Array<AttachmentType>) {
);
}
export function canDisplayImage(attachments?: Array<AttachmentType>) {
export function canDisplayImagePreview(attachments?: Array<AttachmentType>) {
// Note: when we display an image we usually display the preview.
// The preview is usually downscaled
const { height, width } =
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
attachments && attachments[0]?.thumbnail ? attachments[0].thumbnail : { height: 0, width: 0 };
return Boolean(
height &&

@ -2,7 +2,7 @@
import imageType from 'image-type';
import { arrayBufferToBlob } from 'blob-util';
import loadImage, { LoadImageOptions } from 'blueimp-load-image';
import loadImage from 'blueimp-load-image';
import { StagedAttachmentType } from '../components/conversation/composition/CompositionBox';
import { SignalService } from '../protobuf';
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
@ -68,7 +68,7 @@ export async function autoScaleForAvatar<T extends { contentType: string; blob:
}
if (DEBUG_ATTACHMENTS_SCALE) {
window.log.info('autoscale for avatar', maxMeasurements);
window.log.debug('autoscale for avatar', maxMeasurements);
}
return autoScale(attachment, maxMeasurements);
}
@ -96,7 +96,7 @@ export async function autoScaleForIncomingAvatar(incomingAvatar: ArrayBuffer) {
}
if (DEBUG_ATTACHMENTS_SCALE) {
window.log.info('autoscale for incoming avatar', maxMeasurements);
window.log.debug('autoscale for incoming avatar', maxMeasurements);
}
return autoScale(
@ -121,7 +121,7 @@ export async function autoScaleForThumbnail<T extends { contentType: string; blo
};
if (DEBUG_ATTACHMENTS_SCALE) {
window.log.info('autoScaleForThumbnail', maxMeasurements);
window.log.debug('autoScaleForThumbnail', maxMeasurements);
}
return autoScale(attachment, maxMeasurements);
@ -189,44 +189,58 @@ export async function autoScale<T extends { contentType: string; blob: Blob }>(
throw new Error(`GIF is too large, required size is ${maxSize}`);
}
const loadImgOpts: LoadImageOptions = {
maxWidth: makeSquare ? maxMeasurements?.maxSide : maxWidth,
maxHeight: makeSquare ? maxMeasurements?.maxSide : maxHeight,
crop: !!makeSquare,
orientation: 1,
aspectRatio: makeSquare ? 1 : undefined,
canvas: true,
imageSmoothingQuality: 'medium',
};
perfStart(`loadimage-*${blob.size}`);
const canvas = await loadImage(blob, loadImgOpts);
const canvasLoad = await loadImage(blob, {});
const canvasScaled = loadImage.scale(
canvasLoad.image, // img or canvas element
{
maxWidth: makeSquare ? maxMeasurements?.maxSide : maxWidth,
maxHeight: makeSquare ? maxMeasurements?.maxSide : maxHeight,
crop: !!makeSquare,
cover: !!makeSquare,
orientation: 1,
canvas: true,
imageSmoothingQuality: 'medium',
meta: false,
}
);
perfEnd(`loadimage-*${blob.size}`, `loadimage-*${blob.size}`);
if (!canvas || !canvas.originalWidth || !canvas.originalHeight) {
if (!canvasScaled || !canvasScaled.width || !canvasScaled.height) {
throw new Error('failed to scale image');
}
let readAndResizedBlob = blob;
if (
canvas.originalWidth <= maxWidth &&
canvas.originalHeight <= maxHeight &&
canvasScaled.width <= maxWidth &&
canvasScaled.height <= maxHeight &&
blob.size <= maxSize &&
!makeSquare
) {
if (DEBUG_ATTACHMENTS_SCALE) {
window.log.debug('canvasScaled used right away as width, height and size are fine', {
canvasScaledWidth: canvasScaled.width,
canvasScaledHeight: canvasScaled.height,
maxWidth,
maxHeight,
blobsize: blob.size,
maxSize,
makeSquare,
});
}
// the canvas has a size of whatever was given by the caller of autoscale().
// so we have to return those measures as the loaded file has now those measures.
return {
...attachment,
width: canvas.image.width,
height: canvas.image.height,
blob,
contentType: attachment.contentType,
width: canvasScaled.width,
height: canvasScaled.height,
};
}
if (DEBUG_ATTACHMENTS_SCALE) {
window.log.debug('canvas.originalWidth', {
canvasOriginalWidth: canvas.originalWidth,
canvasOriginalHeight: canvas.originalHeight,
window.log.debug('canvasOri.originalWidth', {
canvasOriginalWidth: canvasScaled.width,
canvasOriginalHeight: canvasScaled.height,
maxWidth,
maxHeight,
blobsize: blob.size,
@ -240,10 +254,10 @@ export async function autoScale<T extends { contentType: string; blob: Blob }>(
do {
i -= 1;
if (DEBUG_ATTACHMENTS_SCALE) {
// window.log.info(`autoscale iteration: [${i}] for:`, attachment);
window.log.debug(`autoscale iteration: [${i}] for:`, JSON.stringify(readAndResizedBlob.size));
}
// eslint-disable-next-line no-await-in-loop
const tempBlob = await canvasToBlob(canvas.image as HTMLCanvasElement, 'image/jpeg', quality);
const tempBlob = await canvasToBlob(canvasScaled, 'image/jpeg', quality);
if (!tempBlob) {
throw new Error('Failed to get blob during canvasToBlob.');
@ -265,8 +279,8 @@ export async function autoScale<T extends { contentType: string; blob: Blob }>(
contentType: attachment.contentType,
blob: readAndResizedBlob,
width: canvas.image.width,
height: canvas.image.height,
width: canvasScaled.width,
height: canvasScaled.height,
};
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save