Merge branch 'unstable' into feat/ses-899/user_profile_poll

pull/3056/head
William Grant 1 year ago
commit 97cd21a77e

@ -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",
@ -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);

@ -1,5 +1,5 @@
import classNames from 'classnames';
import { CSSProperties } from 'styled-components';
import React, { CSSProperties } from 'react';
import {
useIsPrivate,
@ -24,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 CSSProperties;
const textProfile = profileName || name || convoName || window.i18n('anonymous');
@ -38,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,3 @@
import { useContext } from 'react';
import styled from 'styled-components';
import {
@ -10,10 +9,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 +45,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 { useContext, useLayoutEffect } from 'react';
import { 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,6 +26,7 @@ import { Message } from './message/message-item/Message';
import { MessageRequestResponse } from './message/message-item/MessageRequestResponse';
import { CallNotification } from './message/message-item/notification-bubble/CallNotification';
import { IsDetailMessageViewContext } from '../../contexts/isDetailViewContext';
import { SessionLastSeenIndicator } from './SessionLastSeenIndicator';
import { TimerNotification } from './TimerNotification';
import { DataExtractionNotification } from './message/message-item/DataExtractionNotification';
@ -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>
);
};

@ -3,7 +3,7 @@ import { contextMenu } from 'react-contexify';
import { connect } from 'react-redux';
import autoBind from 'auto-bind';
import { Component, RefObject, createContext } from 'react';
import { Component, RefObject } from 'react';
import styled from 'styled-components';
import {
ReduxConversationType,
@ -14,6 +14,10 @@ import {
} from '../../state/ducks/conversations';
import { SessionScrollButton } from '../SessionScrollButton';
import {
ScrollToLoadedMessageContext,
ScrollToLoadedReasons,
} from '../../contexts/ScrollToLoadedMessage';
import { StateType } from '../../state/reducer';
import {
getQuotedMessageToAnimate,
@ -30,17 +34,6 @@ export type SessionMessageListProps = {
};
export const messageContainerDomID = 'messages-container';
export type ScrollToLoadedReasons =
| 'quote-or-search-result'
| 'go-to-bottom'
| 'unread-indicator'
| 'load-more-top'
| 'load-more-bottom';
export const ScrollToLoadedMessageContext = 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,17 +1,13 @@
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import moment from 'moment';
import {
MouseEvent,
createContext,
useCallback,
useContext,
useLayoutEffect,
useState,
} from 'react';
import { MouseEvent, 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 {
@ -25,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';
@ -41,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()
@ -83,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) =>
@ -98,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);
@ -161,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
@ -171,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}`}
@ -191,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 { MouseEvent, 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>

@ -2,6 +2,7 @@ import { isEmpty, toNumber } from 'lodash';
import { MouseEvent } 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';
@ -20,6 +21,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;
@ -39,6 +41,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,3 @@
import { ReactElement } from 'react';
import styled from 'styled-components';
import { isEmpty } from 'lodash';
@ -160,7 +159,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 { ReactElement, useEffect, useState } from 'react';
import { 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);

@ -17,6 +17,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 { 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 { MouseEvent, 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}
/>

@ -10,7 +10,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) => {
@ -24,11 +23,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} />;
};

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

@ -5,12 +5,12 @@ import {
MouseEventHandler,
ReactNode,
useCallback,
useContext,
useLayoutEffect,
useState,
} from 'react';
import { InView } from 'react-intersection-observer';
import { useDispatch, useSelector } from 'react-redux';
import { useScrollToLoadedMessage } from '../../../../contexts/ScrollToLoadedMessage';
import { Data } from '../../../../data/data';
import { useHasUnread } from '../../../../hooks/useParamSelector';
import { getConversationController } from '../../../../session/conversations';
@ -30,7 +30,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: ReactNode;
@ -97,7 +96,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 { ReactElement, useRef, useState } from 'react';
import { 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,4 +1,4 @@
import { ReactElement, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { Data } from '../../../../data/data';
@ -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) => {

@ -1,7 +1,7 @@
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;
@ -29,8 +29,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');
@ -38,7 +42,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>

@ -13,6 +13,7 @@ import {
useMessageHash,
useMessageReceivedAt,
useMessageSender,
useMessageSenderIsAdmin,
useMessageServerId,
useMessageServerTimestamp,
useMessageTimestamp,
@ -110,6 +111,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;
@ -136,7 +138,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, ReactElement, useState } from 'react';
import { ChangeEvent, MouseEvent, 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,18 +1,20 @@
import { shell } from 'electron';
import { ipcRenderer, shell } from 'electron';
import { 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';
@ -75,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} />;
}
@ -103,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 (
@ -118,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>
@ -191,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,4 +1,4 @@
import { ReactElement, useState } from 'react';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { useMessageReactsPropsById } from '../../hooks/useParamSelector';
@ -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,5 +1,5 @@
import { isEmpty, isEqual } from 'lodash';
import { ReactElement, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { Data } from '../../data/data';
@ -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();

@ -15,6 +15,7 @@ interface SProps extends SessionIconProps {
dataTestId?: string;
dataTestIdIcon?: string;
id?: string;
title?: string;
style?: object;
tabIndex?: number;
}
@ -57,6 +58,7 @@ const SessionIconButtonInner = forwardRef<HTMLDivElement, SProps>((props, ref) =
iconPadding,
margin,
id,
title,
dataTestId,
dataTestIdIcon,
style,
@ -83,6 +85,7 @@ const SessionIconButtonInner = forwardRef<HTMLDivElement, SProps>((props, ref) =
role="button"
ref={ref}
id={id}
title={title}
onClick={clickHandler}
style={{ ...style, display: isHidden ? 'none' : 'flex', margin: margin || '' }}
tabIndex={tabIndex}

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

@ -3,6 +3,7 @@ import classNames from 'classnames';
import { MouseEvent } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext';
import { Data } from '../../../data/data';
import {
useActiveAt,
@ -21,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 = () => {

@ -2,6 +2,7 @@ import classNames from 'classnames';
import { isEmpty } from 'lodash';
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,4 +1,5 @@
import { useSelector } from 'react-redux';
import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext';
import {
useConversationRealName,
useConversationUsername,
@ -8,7 +9,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();
@ -35,15 +35,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 { 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';

@ -1,12 +1,12 @@
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 { SessionContextMenuContainer } from '../SessionContextMenuContainer';
import { useConvoIdFromContext } from '../leftpane/conversation-list-item/ConvoIdContext';
import {
AcceptMsgRequestMenuItem,
BanMenuItem,

@ -1,5 +1,6 @@
import { Item, Submenu } from 'react-contexify';
import { useDispatch, useSelector } from 'react-redux';
import { useConvoIdFromContext } from '../../contexts/ConvoIdContext';
import {
useAvatarPath,
useConversationUsername,
@ -54,7 +55,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 */

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

@ -64,14 +64,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}

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

@ -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 => {

@ -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({

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

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