Merge remote-tracking branch 'origin/unstable' into fix-resize-images-thumbnail

pull/2887/head
Audric Ackermann 8 months ago
commit 29185c1aa8

@ -2,6 +2,11 @@ module.exports = {
root: true,
settings: {
'import/core-modules': ['electron'],
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
react: {
version: 'detect',
},

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit "$1"

@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Disabling this hook for now because the BuildLintCommand has issues. If you have an error in a file that eslint catches the commit fails silently instead of explaining the error (Will 04/09/2023)
# npx lint-staged

@ -0,0 +1,42 @@
const { ESLint } = require('eslint');
const removeIgnoredFiles = async files => {
const eslint = new ESLint();
const isIgnored = await Promise.all(
files.map(file => {
return eslint.isPathIgnored(file);
})
);
const filteredFiles = files.filter((_, i) => !isIgnored[i]);
return filteredFiles.join(' ');
};
const buildFormatCommand = async files => {
const filesToLint = await removeIgnoredFiles(files);
if (!filesToLint || !filesToLint.length) {
return '';
}
const results = filesToLint.map(f => path.relative(process.cwd(), f));
return results.length
? `prettier --ignore-unknown --list-different --write ${results.join(' ')}`
: '';
};
const buildLintCommand = async files => {
const filesToLint = await removeIgnoredFiles(files);
if (!filesToLint || !filesToLint.length) {
return '';
}
const results = filesToLint.map(f => path.relative(process.cwd(), f));
return results.length ? `eslint --cache ${results.join(' ')}` : '';
};
module.exports = {
'*.{css,js,json,scss,ts,tsx}': [buildFormatCommand, buildLintCommand],
};

@ -37,3 +37,7 @@ Gruntfile.js
!istanbul-reports/lib/html/assets
!istanbul-api/node_modules/istanbul-reports/lib/html/assets
# lint-staged fix
# https://github.com/eemeli/yaml/issues/384#issuecomment-1368496106
!**/yaml/dist/**/doc

@ -10,7 +10,7 @@ It's a good idea to gauge interest in your intended work by finding the current
for it or creating a new one yourself. Use Github issues as a place to signal
your intentions and get feedback from the users most likely to appreciate your changes.
You're most likely to have your pull request accepted if it addresses an existing Github issue marked with the [good-first-issue](https://github.com/oxen-io/session-desktop/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) tag, these issues are specifically tagged, because they are generally features/bug fixes which can be cleanly merged on a single platform without requiring cross platform work, are generally of lower complexity than larger features and are non contentious, meaning that the core team doesn't need to try and assess the community desire for such a feature before merging.
You're most likely to have your pull request accepted if it addresses an existing Github issue marked with the [good-first-issue](https://github.com/oxen-io/session-desktop/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) tag, these issues are specifically tagged, because they are generally features/bug fixes which can be cleanly merged on a single platform without requiring cross platform work, are generally of lower complexity than larger features and are non contentious, meaning that the core team doesn't need to try and assess the community desire for such a feature before merging.
Of course we encourage community developers to work on ANY issue filed on our Github regardless of how its tagged, however if you pick up or create an issue without the “Good first issue” tag it would be best if you leave a comment on the issue so that the core team can give you any guidance required, especially around UI heavy features or issues which require cross platform integration.
@ -34,22 +34,12 @@ Then you need `git`, if you don't have that yet: https://git-scm.com/
### Windows
Building on Windows versions 8+ is supported out of the box
Building on Windows is a pain, but is possible see our CI/Windows build machine prerequisites here [Windows-2022 GH image](https://github.com/actions/runner-images/blob/main/images/win/Windows2022-Readme.md)
### Linux
1. Pick your favorite package manager.
1. Install `python`
1. Install `gcc`
1. Install `g++`
1. Install `make`
1. Depending on your distro, you might need to install `hunspell` and `hunspell-<lan>` (e.g. `hunspell-en-au`)
If you are using a Debian based Linux distribution gcc, g++ and make can be installed as part of the `build-essential` package using
```
apt install build-essential
```
1. Install build tools `apt install build-essential cmake` (this installs make, g++, gcc)
2. Depending on your distro, you might need to install `hunspell` and `hunspell-<lan>` (e.g. `hunspell-en-au`)
### All platforms
@ -129,6 +119,16 @@ Please write tests! Our testing framework is
The easiest way to run all tests at once is `yarn test`.
## Committing your changes
Before a commit is accepted the staged changes will be formatted using [prettier](https://prettier.io/) and linted using [eslint](https://eslint.org/). The commit will be reverted if files are formatted or lint errors are returned.
### Commit Message Convention
This project follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
Commit messages will be checked using [husky](https://typicode.github.io/husky/#/) and [commitlint](https://commitlint.js.org/).
## Pull requests
So you wanna make a pull request? Please observe the following guidelines.

@ -12,6 +12,10 @@ Session integrates directly with [Oxen Service Nodes](https://docs.oxen.io/about
Please search for any [existing issues](https://github.com/oxen-io/session-desktop/issues) that describe your bug in order to avoid duplicate submissions. <br><br>Submissions can be made by making a pull request to our development branch.If you don't know where to start contributing please read [Contributing.md](CONTRIBUTING.md) and refer to issues tagged with the [Good-first-issue](https://github.com/oxen-io/session-desktop/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) tag.
## Supported platforms
Session requires Windows 10 or later, macOS Catalina (10.15) or later, or a linux distribution with glibc 2.28 or later like Debian 10 or Ubuntu 20.04.
## Build instruction
Build instructions can be found in [Contributing.md](CONTRIBUTING.md).
@ -21,7 +25,7 @@ Build instructions can be found in [Contributing.md](CONTRIBUTING.md).
Get Kee's key and import it:
```
wget https://raw.githubusercontent.com/oxen-io/oxen-core/master/utils/gpg_keys/KeeJef.asc
wget https://raw.githubusercontent.com/oxen-io/oxen-core/dev/utils/gpg_keys/KeeJef.asc
gpg --import KeeJef.asc
```

Binary file not shown.

@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

@ -2,7 +2,7 @@
"name": "session-desktop",
"productName": "Session",
"description": "Private messaging from your desktop",
"version": "1.11.1",
"version": "1.11.4",
"license": "GPL-3.0",
"author": {
"name": "Oxen Labs",
@ -64,7 +64,8 @@
"postinstall": "yarn patch-package && yarn electron-builder install-app-deps",
"update-git-info": "node ./build/updateLocalConfig.js",
"worker:utils": "webpack --config=./utils.worker.config.js",
"worker:libsession": "rimraf 'ts/webworker/workers/node/libsession/*.node' && webpack --config=./libsession.worker.config.js"
"worker:libsession": "rimraf 'ts/webworker/workers/node/libsession/*.node' && webpack --config=./libsession.worker.config.js",
"prepare": "husky install"
},
"dependencies": {
"@emoji-mart/data": "^1.1.2",
@ -108,7 +109,7 @@
"protobufjs": "^7.2.4",
"rc-slider": "^10.2.1",
"react": "^17.0.2",
"react-contexify": "5.0.0",
"react-contexify": "^6.0.0",
"react-dom": "^17.0.2",
"react-draggable": "^4.4.4",
"react-h5-audio-player": "^3.2.0",
@ -132,6 +133,9 @@
"webrtc-adapter": "^4.1.1"
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@commitlint/types": "^17.4.4",
"@electron/notarize": "^2.1.0",
"@types/backbone": "1.4.2",
"@types/blueimp-load-image": "^5.16.2",
@ -171,7 +175,7 @@
"cross-env": "^6.0.3",
"css-loader": "^6.7.2",
"dmg-builder": "23.6.0",
"electron": "25.3.0",
"electron": "^25.8.4",
"electron-builder": "23.0.8",
"eslint": "^8.45.0",
"eslint-config-airbnb-base": "^15.0.0",
@ -182,8 +186,10 @@
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"events": "^3.3.0",
"husky": "^8.0.0",
"jsdom": "^22.1.0",
"jsdom-global": "^3.0.2",
"lint-staged": "^14.0.1",
"mini-css-extract-plugin": "^2.7.5",
"mocha": "10.0.0",
"node-loader": "^2.0.0",

@ -6,7 +6,7 @@ body.rtl {
.group-settings-item,
.contact-selection-list,
.group-member-list__selection,
.react-contexify__item {
.contexify_item {
direction: rtl;
}

@ -1,6 +1,6 @@
// Modules
@import 'react-h5-audio-player/lib/styles.css';
@import 'react-contexify/dist/ReactContexify.min.css';
@import 'react-contexify/dist/ReactContexify.css'; // the .css is actually referencing the .min.css in the 6.0.0
@import 'react-toastify/dist/ReactToastify.css';
@import 'sanitize.css/sanitize.css';

@ -3,36 +3,35 @@ import styled from 'styled-components';
export const SessionContextMenuContainer = styled.div.attrs({
// custom props
})`
.react-contexify {
.contexify {
// be sure it is more than the one set for the More Information screen of messages
z-index: 30;
min-width: 200px;
box-shadow: 0px 0px 10px var(--context-menu-shadow-color) !important;
background-color: var(--context-menu-background-color);
&.react-contexify__theme--dark {
&.contexify_theme-dark {
background-color: var(--context-menu-background-color);
}
.react-contexify__item {
.contexify_item {
background: var(--context-menu-background-color);
}
.react-contexify__item:not(.react-contexify__item--disabled):hover
> .react-contexify__item__content {
.contexify_item:not(.contexify_item-disabled):hover > .contexify_itemContent {
background: var(--context-menu-background-hover-color);
color: var(--context-menu-text-hover-color);
}
.react-contexify__item__content {
.contexify_itemContent {
transition: var(--default-duration);
color: var(--context-menu-text-color);
}
&.react-contexify__submenu {
&.contexify_submenu {
top: -28px !important; // height of an item element
}
.react-contexify__submenu-arrow {
.contexify_submenu-arrow {
line-height: 16px; // center the arrow for submenu
}
}

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { animation, contextMenu, Item, Menu } from 'react-contexify';
import { contextMenu, Item, Menu } from 'react-contexify';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
@ -20,7 +20,7 @@ const VideoInputMenu = ({
}) => {
return (
<SessionContextMenuContainer>
<Menu id={triggerId} animation={animation.fade}>
<Menu id={triggerId} animation="fade">
{camerasList.map(m => {
return (
<Item
@ -93,7 +93,7 @@ const AudioInputMenu = ({
}) => {
return (
<SessionContextMenuContainer>
<Menu id={triggerId} animation={animation.fade}>
<Menu id={triggerId} animation="fade">
{audioInputsList.map(m => {
return (
<Item
@ -162,7 +162,7 @@ const AudioOutputMenu = ({
}) => {
return (
<SessionContextMenuContainer>
<Menu id={triggerId} animation={animation.fade}>
<Menu id={triggerId} animation="fade">
{audioOutputsList.map(m => {
return (
<Item

@ -86,7 +86,7 @@ const StyledMessageOpaqueContent = styled(StyledMessageHighlighter)<{
align-self: ${props => (props.messageDirection === 'incoming' ? 'flex-start' : 'flex-end')};
padding: var(--padding-message-content);
border-radius: var(--border-radius-message-box);
max-width: 100%;
width: 100%;
`;
export const IsMessageVisibleContext = createContext(false);

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { animation, Item, Menu, useContextMenu } from 'react-contexify';
import { Item, ItemParams, Menu, useContextMenu } from 'react-contexify';
import { useDispatch } from 'react-redux';
import { useClickAway, useMouse } from 'react-use';
import styled from 'styled-components';
import { isNumber } from 'lodash';
import { Data } from '../../../../data/data';
import { MessageInteraction } from '../../../../interactions';
@ -24,18 +24,6 @@ import {
showMessageDetailsView,
toggleSelectedMessageId,
} from '../../../../state/ducks/conversations';
import {
useSelectedConversationKey,
useSelectedIsBlocked,
useSelectedIsPublic,
useSelectedWeAreAdmin,
useSelectedWeAreModerator,
} from '../../../../state/selectors/selectedConversation';
import { saveAttachmentToDisk } from '../../../../util/attachmentsUtil';
import { Reactions } from '../../../../util/reactions';
import { SessionContextMenuContainer } from '../../../SessionContextMenuContainer';
import { SessionEmojiPanel, StyledEmojiPanel } from '../../SessionEmojiPanel';
import { MessageReactBar } from './MessageReactBar';
import {
useMessageAttachments,
useMessageBody,
@ -48,7 +36,18 @@ import {
useMessageStatus,
useMessageTimestamp,
} from '../../../../state/selectors';
import { useIsPublic } from '../../../../hooks/useParamSelector';
import {
useSelectedConversationKey,
useSelectedIsBlocked,
useSelectedIsPublic,
useSelectedWeAreAdmin,
useSelectedWeAreModerator,
} from '../../../../state/selectors/selectedConversation';
import { saveAttachmentToDisk } from '../../../../util/attachmentsUtil';
import { Reactions } from '../../../../util/reactions';
import { SessionContextMenuContainer } from '../../../SessionContextMenuContainer';
import { SessionEmojiPanel, StyledEmojiPanel } from '../../SessionEmojiPanel';
import { MessageReactBar } from './MessageReactBar';
export type MessageContextMenuSelectorProps = Pick<
MessageRenderingProps,
@ -67,7 +66,7 @@ type Props = { messageId: string; contextMenuId: string; enableReactions: boolea
const StyledMessageContextMenu = styled.div`
position: relative;
.react-contexify {
.contexify {
margin-left: -104px;
}
`;
@ -104,55 +103,13 @@ const DeleteForEveryone = ({ messageId }: { messageId: string }) => {
type MessageId = { messageId: string };
const SaveAttachment = ({ messageId }: MessageId) => {
const convoId = useSelectedConversationKey();
const attachments = useMessageAttachments(messageId);
const timestamp = useMessageTimestamp(messageId);
const serverTimestamp = useMessageServerTimestamp(messageId);
const sender = useMessageSender(messageId);
const saveAttachment = useCallback(
(e: any) => {
// this is quite dirty but considering that we want the context menu of the message to show on click on the attachment
// and the context menu save attachment item to save the right attachment I did not find a better way for now.
let targetAttachmentIndex = e.triggerEvent.path[1].getAttribute('data-attachmentindex');
e.event.stopPropagation();
if (!attachments?.length || !convoId || !sender) {
return;
}
if (!targetAttachmentIndex) {
targetAttachmentIndex = 0;
}
if (targetAttachmentIndex > attachments.length) {
return;
}
const messageTimestamp = timestamp || serverTimestamp || 0;
void saveAttachmentToDisk({
attachment: attachments[targetAttachmentIndex],
messageTimestamp,
messageSender: sender,
conversationId: convoId,
});
},
[convoId, sender, attachments, serverTimestamp, timestamp]
);
if (!convoId) {
return null;
}
return attachments?.length ? (
<Item onClick={saveAttachment}>{window.i18n('downloadAttachment')}</Item>
) : null;
};
const AdminActionItems = ({ messageId }: MessageId) => {
const convoId = useSelectedConversationKey();
const isPublic = useIsPublic();
const isPublic = useSelectedIsPublic();
const weAreModerator = useSelectedWeAreModerator();
const weAreAdmin = useSelectedWeAreAdmin();
const showAdminActions = (weAreAdmin || weAreModerator) && isPublic;
const sender = useMessageSender(messageId);
const isSenderAdmin = useMessageSenderIsAdmin(messageId);
@ -218,6 +175,11 @@ export const MessageContextMenu = (props: Props) => {
const status = useMessageStatus(messageId);
const isDeletable = useMessageIsDeletable(messageId);
const text = useMessageBody(messageId);
const attachments = useMessageAttachments(messageId);
const timestamp = useMessageTimestamp(messageId);
const serverTimestamp = useMessageServerTimestamp(messageId);
const sender = useMessageSender(messageId);
const isOutgoing = direction === 'outgoing';
const isSent = status === 'sent' || status === 'read'; // a read message should be replyable
@ -233,21 +195,24 @@ export const MessageContextMenu = (props: Props) => {
const [mouseX, setMouseX] = useState(0);
const [mouseY, setMouseY] = useState(0);
const onContextMenuShown = () => {
if (showEmojiPanel) {
setShowEmojiPanel(false);
}
window.contextMenuShown = true;
};
const onContextMenuHidden = useCallback(() => {
// This function will called before the click event
// on the message would trigger (and I was unable to
// prevent propagation in this case), so use a short timeout
setTimeout(() => {
window.contextMenuShown = false;
}, 100);
}, []);
const onVisibilityChange = useCallback(
(isVisible: boolean) => {
if (isVisible) {
if (showEmojiPanel) {
setShowEmojiPanel(false);
}
window.contextMenuShown = true;
return;
}
// This function will called before the click event
// on the message would trigger (and I was unable to
// prevent propagation in this case), so use a short timeout
setTimeout(() => {
window.contextMenuShown = false;
}, 100);
},
[showEmojiPanel]
);
const onShowDetail = async () => {
const found = await Data.getMessageById(messageId);
@ -313,6 +278,30 @@ export const MessageContextMenu = (props: Props) => {
}
};
const saveAttachment = (e: ItemParams) => {
// this is quite dirty but considering that we want the context menu of the message to show on click on the attachment
// and the context menu save attachment item to save the right attachment I did not find a better way for now.
// Note: If you change this, also make sure to update the `handleContextMenu()` in GenericReadableMessage.tsx
const targetAttachmentIndex = isNumber(e?.props?.dataAttachmentIndex)
? e.props.dataAttachmentIndex
: 0;
e.event.stopPropagation();
if (!attachments?.length || !convoId || !sender) {
return;
}
if (targetAttachmentIndex > attachments.length) {
return;
}
const messageTimestamp = timestamp || serverTimestamp || 0;
void saveAttachmentToDisk({
attachment: attachments[targetAttachmentIndex],
messageTimestamp,
messageSender: sender,
conversationId: convoId,
});
};
useClickAway(emojiPanelRef, () => {
onEmojiLoseFocus();
});
@ -360,18 +349,14 @@ export const MessageContextMenu = (props: Props) => {
</StyledEmojiPanelContainer>
)}
<SessionContextMenuContainer>
<Menu
id={contextMenuId}
onShown={onContextMenuShown}
onHidden={onContextMenuHidden}
animation={animation.fade}
>
<Menu id={contextMenuId} onVisibilityChange={onVisibilityChange} animation="fade">
{enableReactions && (
// eslint-disable-next-line @typescript-eslint/no-misused-promises
<MessageReactBar action={onEmojiClick} additionalAction={onShowEmoji} />
)}
<SaveAttachment messageId={messageId} />
{attachments?.length ? (
<Item onClick={saveAttachment}>{window.i18n('downloadAttachment')}</Item>
) : null}
<Item onClick={copyText}>{window.i18n('copyMessage')}</Item>
{(isSent || !isOutgoing) && (
<Item onClick={onReply}>{window.i18n('replyToMessage')}</Item>

@ -7,6 +7,7 @@ import styled, { keyframes } from 'styled-components';
import useInterval from 'react-use/lib/useInterval';
import useMount from 'react-use/lib/useMount';
import { isNil, isString, toNumber } from 'lodash';
import { Data } from '../../../../data/data';
import { MessageRenderingProps } from '../../../../models/messageType';
import { getConversationController } from '../../../../session/conversations';
@ -179,13 +180,27 @@ export const GenericReadableMessage = (props: Props) => {
const handleContextMenu = useCallback(
(e: React.MouseEvent<HTMLElement>) => {
// this is quite dirty but considering that we want the context menu of the message to show on click on the attachment
// and the context menu save attachment item to save the right attachment I did not find a better way for now.
// Note: If you change this, also make sure to update the `saveAttachment()` in MessageContextMenu.tsx
const enableContextMenu = !multiSelectMode && !msgProps?.isKickedFromGroup;
const attachmentIndexStr = (e?.target as any)?.parentElement?.getAttribute?.(
'data-attachmentindex'
);
const attachmentIndex =
isString(attachmentIndexStr) && !isNil(toNumber(attachmentIndexStr))
? toNumber(attachmentIndexStr)
: 0;
if (enableContextMenu) {
contextMenu.hideAll();
contextMenu.show({
id: ctxMenuID,
event: e,
props: {
dataAttachmentIndex: attachmentIndex,
},
});
}
setIsRightClicked(enableContextMenu);

@ -86,7 +86,7 @@ export const OverlayMessage = () => {
const pubkeyorOnsTrimmed = pubkeyOrOns.trim();
if (!PubKey.validateWithErrorNoBlinding(pubkeyorOnsTrimmed)) {
await openConvoOnceResolved(pubkeyOrOns);
await openConvoOnceResolved(pubkeyorOnsTrimmed);
return;
}

@ -1,5 +1,5 @@
import React from 'react';
import { animation, Item, Menu, Submenu } from 'react-contexify';
import { Item, Menu, Submenu } from 'react-contexify';
import { useSelector } from 'react-redux';
import { setDisappearingMessagesByConvoId } from '../../interactions/conversationInteractions';
import { isSearching } from '../../state/selectors/search';
@ -64,7 +64,7 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
return (
<ContextConversationProvider value={convoId}>
<SessionContextMenuContainer>
<Menu id={triggerId} animation={animation.fade}>
<Menu id={triggerId} animation="fade">
<DisappearingMessageMenuItem />
<NotificationForConvoMenuItem />
<BlockMenuItem />

@ -1,5 +1,5 @@
import React from 'react';
import { animation, Item, Menu } from 'react-contexify';
import { Item, Menu } from 'react-contexify';
import { useSelector } from 'react-redux';
import { useIsPinned, useIsPrivate, useIsPrivateAndFriend } from '../../hooks/useParamSelector';
@ -44,7 +44,7 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
return (
<SessionContextMenuContainer>
<Menu id={triggerId} animation={animation.fade}>
<Menu id={triggerId} animation="fade">
{/* Message request related actions */}
<AcceptMsgRequestMenuItem />
<DeclineMsgRequestMenuItem />

@ -1,5 +1,5 @@
import React from 'react';
import { animation, Item, Menu } from 'react-contexify';
import { Item, Menu } from 'react-contexify';
import { useDispatch } from 'react-redux';
import { SessionContextMenuContainer } from '../SessionContextMenuContainer';
@ -28,7 +28,7 @@ export const MessageRequestBannerContextMenu = (props: PropsContextConversationI
return (
<SessionContextMenuContainer>
<Menu id={triggerId} animation={animation.fade}>
<Menu id={triggerId} animation="fade">
<HideBannerMenuItem />
</Menu>
</SessionContextMenuContainer>

@ -530,7 +530,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
// an OpenGroupV2 message is just a visible message
const chatMessageParams: VisibleMessageParams = {
body: '',
timestamp: sentAt,
// we need to use a new timestamp here, otherwise android&iOS will consider this message as a duplicate and drop the synced reaction
timestamp: GetNetworkTime.getNowWithNetworkOffset(),
reaction,
lokiProfile: UserUtils.getOurProfile(),
};

@ -4,10 +4,12 @@ import path from 'path';
import * as BetterSqlite3 from '@signalapp/better-sqlite3';
import rimraf from 'rimraf';
import { base64_variants, from_base64, to_hex } from 'libsodium-wrappers-sumo';
import {
chunk,
compact,
difference,
differenceBy,
forEach,
fromPairs,
isArray,
@ -20,7 +22,6 @@ import {
omit,
uniq,
} from 'lodash';
import { base64_variants, from_base64, to_hex } from 'libsodium-wrappers-sumo';
import { ConversationAttributes } from '../models/conversationAttributes';
import { PubKey } from '../session/types/PubKey'; // checked - only node
@ -60,11 +61,13 @@ import {
} from '../types/sqlSharedTypes';
import { KNOWN_BLINDED_KEYS_ITEM, SettingsKey } from '../data/settings-key';
import { Quote } from '../receiver/types';
import {
getSQLCipherIntegrityCheck,
openAndMigrateDatabase,
updateSchema,
} from './migration/signalMigrations';
import { configDumpData } from './sql_calls/config_dump';
import {
assertGlobalInstance,
assertGlobalInstanceOrInstance,
@ -72,8 +75,6 @@ import {
initDbInstanceWith,
isInstanceInitialized,
} from './sqlInstance';
import { configDumpData } from './sql_calls/config_dump';
import { Quote } from '../receiver/types';
// eslint:disable: function-name non-literal-fs-path
@ -620,11 +621,28 @@ function getAllConversations() {
.prepare(`SELECT * FROM ${CONVERSATIONS_TABLE} ORDER BY id ASC;`)
.all();
return (rows || []).map(m => {
const unreadCount = getUnreadCountByConversation(m.id) || 0;
const mentionedUsStillUnread = !!getFirstUnreadMessageWithMention(m.id);
return formatRowOfConversation(m, 'getAllConversations', unreadCount, mentionedUsStillUnread);
const formatted = compact(
(rows || []).map(m => {
const unreadCount = getUnreadCountByConversation(m.id) || 0;
const mentionedUsStillUnread = !!getFirstUnreadMessageWithMention(m.id);
return formatRowOfConversation(m, 'getAllConversations', unreadCount, mentionedUsStillUnread);
})
);
const invalidOnLoad = formatted.filter(m => {
return isString(m.id) && m.id.startsWith('05') && m.id.includes(' ');
});
if (!isEmpty(invalidOnLoad)) {
const idsInvalid = invalidOnLoad.map(m => m.id);
console.info(
'getAllConversations removing those conversations with invalid ids before load',
idsInvalid
);
removeConversation(idsInvalid);
}
return differenceBy(formatted, invalidOnLoad, c => c.id);
}
function getPubkeysInPublicConversation(conversationId: string) {

@ -35,6 +35,7 @@ import { getAllCachedECKeyPair, sentAtMoreRecentThanWrapper } from './closedGrou
import { ConfigMessageHandler } from './configMessage';
import { ECKeyPair } from './keypairs';
import { ContactsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
import { isUsFromCache } from '../session/utils/User';
export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) {
try {
@ -292,27 +293,42 @@ async function decrypt(envelope: EnvelopePlus): Promise<any> {
return plaintext;
}
async function shouldDropIncomingPrivateMessage(sentAtTimestamp: number, envelope: EnvelopePlus) {
async function shouldDropIncomingPrivateMessage(
sentAtTimestamp: number,
envelope: EnvelopePlus,
content: SignalService.Content
) {
// sentAtMoreRecentThanWrapper is going to be true, if the latest contact wrapper we processed was roughly more recent that this message timestamp
const moreRecentOrNah = await sentAtMoreRecentThanWrapper(sentAtTimestamp, 'ContactsConfig');
const isSyncedMessage = isUsFromCache(envelope.source);
if (moreRecentOrNah === 'wrapper_more_recent') {
// we need to check if that conversation is already in the wrapper, and if yes
// we need to check if that conversation is already in the wrapper
try {
const privateConvoInWrapper = await ContactsWrapperActions.get(envelope.source);
// let's check if the corresponding conversation is hidden in the contacts wrapper or not.
// the corresponding conversation is syncTarget when this is a synced message only, so we need to rely on it first, then the envelope.source.
const syncTargetOrSource = isSyncedMessage
? content.dataMessage?.syncTarget || undefined
: envelope.source;
if (!syncTargetOrSource) {
return false;
}
const privateConvoInWrapper = await ContactsWrapperActions.get(syncTargetOrSource);
if (
!privateConvoInWrapper ||
privateConvoInWrapper.priority <= CONVERSATION_PRIORITIES.hidden
) {
// the wrapper is more recent that this message and there is no such private conversation. Just drop this incoming message.
window.log.info(
`received message from contact ${envelope.source} which appears to be hidden/removed in our most recent libsession contactconfig, sentAt: ${sentAtTimestamp}. Dropping it`
`received message on conversation ${syncTargetOrSource} which appears to be hidden/removed in our most recent libsession contactconfig, sentAt: ${sentAtTimestamp}. Dropping it`
);
return true;
}
window.log.info(
`received message from contact ${envelope.source} which appears to NOT be hidden/removed in our most recent libsession contactconfig, sentAt: ${sentAtTimestamp}. `
`received message on conversation ${syncTargetOrSource} which appears to NOT be hidden/removed in our most recent libsession contactconfig, sentAt: ${sentAtTimestamp}. `
);
} catch (e) {
window.log.warn(
@ -409,7 +425,7 @@ export async function innerHandleSwarmContentMessage(
const isPrivateConversationMessage = !envelope.senderIdentity;
if (isPrivateConversationMessage) {
if (await shouldDropIncomingPrivateMessage(sentAtTimestamp, envelope)) {
if (await shouldDropIncomingPrivateMessage(sentAtTimestamp, envelope, content)) {
await removeFromCache(envelope);
return;
}

@ -29,6 +29,11 @@ const mappedContactWrapperValues = new Map<string, ContactInfo>();
* We want to sync the message request status so we need to allow a contact even if it's not approved, did not approve us and is not blocked.
*/
function isContactToStoreInWrapper(convo: ConversationModel): boolean {
try {
PubKey.cast(convo.id as string);
} catch (e) {
return false;
}
return !convo.isMe() && convo.isPrivate() && convo.isActive() && !PubKey.isBlinded(convo.id);
}

@ -13,7 +13,7 @@ describe('libsession_contacts', () => {
describe('filter contacts for wrapper', () => {
const ourNumber = '051234567890acbdef';
const validArgs = {
id: '051111567890acbdef',
id: '050123456789abcdef050123456789abcdef0123456789abcdef050123456789ab',
type: ConversationTypeEnum.PRIVATE,
isApproved: true,
active_at: 123,
@ -137,6 +137,58 @@ describe('libsession_contacts', () => {
).to.be.eq(false);
});
it('excludes contacts not matching a pubkey syntax (space in middle)', () => {
const validIdWithSpaceInIt =
'050123456789abcdef050123456789 bcdef0123456789abcdef050123456789ab'; // len 66 but has a ' ' in the middle
expect(
SessionUtilContact.isContactToStoreInWrapper(
new ConversationModel({
...validArgs,
id: validIdWithSpaceInIt,
} as any)
)
).to.be.eq(false);
});
it('excludes contacts not matching a pubkey syntax (space at the end)', () => {
const validIdWithSpaceInIt =
'050123456789abcdef050123456789abcdef0123456789abcdef050123456789a '; // len 66 but has a ' ' at the end
expect(
SessionUtilContact.isContactToStoreInWrapper(
new ConversationModel({
...validArgs,
id: validIdWithSpaceInIt,
} as any)
)
).to.be.eq(false);
});
it('excludes contacts not matching a pubkey syntax (space at the start)', () => {
const validIdWithSpaceInIt =
' 050123456789abcdef050123456789abcdef0123456789abcdef050123456789ab'; // len 66 but has a ' ' at the start
expect(
SessionUtilContact.isContactToStoreInWrapper(
new ConversationModel({
...validArgs,
id: validIdWithSpaceInIt,
} as any)
)
).to.be.eq(false);
});
it('excludes contacts not matching a pubkey syntax (non hex char)', () => {
const validIdWithSpaceInIt =
'050123456789abcdef050123456789abcdef0123456789abcdef050123456789aU'; // len 66 but has 'U' at the end
expect(
SessionUtilContact.isContactToStoreInWrapper(
new ConversationModel({
...validArgs,
id: validIdWithSpaceInIt,
} as any)
)
).to.be.eq(false);
});
it('includes approved only by them ', () => {
expect(
SessionUtilContact.isContactToStoreInWrapper(

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