Merge branch 'clearnet' into small-refactor

pull/2131/head
Audric Ackermann 2 years ago committed by GitHub
commit 15ae511bca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -24,3 +24,5 @@ test/blanket_mocha.js
# TypeScript generated files
ts/**/*.js
**/ts/**/*.js
playwright.config.js

5
.gitignore vendored

@ -27,6 +27,8 @@ ts/protobuf/*.d.ts
ts/**/*.js.map
test/ts/**/*.js.map
test/ts/**/*.js
ts/test/automation/**/*.js
ts/test/automation/**/*.js.map
# Swapfiles
@ -45,3 +47,6 @@ yarn-error.log
.vscode/
libloki/test/test.js
playwright.config.js
playwright.config.js.map

@ -19,6 +19,7 @@ ts/protobuf/*.d.ts
ts/protobuf/*.js
stylesheets/manifest.css
ts/util/lint/exceptions.json
ts/test/automation/notes
# Third-party files
node_modules/**

@ -3,6 +3,7 @@ __tests__
test
tests
powered-test
!@playwright/test/**
# asset directories
docs

@ -462,5 +462,8 @@
"callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.",
"menuCall": "Call",
"startedACall": "You called $name$",
"answeredACall": "Call with $name$"
"answeredACall": "Call with $name$",
"trimDatabase": "Trim Database",
"trimDatabaseDescription": "Reduces your message database size to your last 10,000 messages.",
"trimDatabaseConfirmationBody": "Are you sure you want to delete your $deleteAmount$ oldest received messages?"
}

@ -73,6 +73,8 @@ module.exports = {
getMessagesByConversation,
getFirstUnreadMessageIdInConversation,
hasConversationOutgoingMessage,
trimMessages,
fillWithTestData,
getUnprocessedCount,
getAllUnprocessed,
@ -836,6 +838,8 @@ const LOKI_SCHEMA_VERSIONS = [
updateToLokiSchemaVersion15,
updateToLokiSchemaVersion16,
updateToLokiSchemaVersion17,
updateToLokiSchemaVersion18,
updateToLokiSchemaVersion19,
];
function updateToLokiSchemaVersion1(currentVersion, db) {
@ -1251,6 +1255,86 @@ function updateToLokiSchemaVersion17(currentVersion, db) {
console.log(`updateToLokiSchemaVersion${targetVersion}: success!`);
}
function updateToLokiSchemaVersion18(currentVersion, db) {
const targetVersion = 18;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`);
// Dropping all pre-existing schema relating to message searching.
// Recreating the full text search and related triggers
db.transaction(() => {
db.exec(`
DROP TRIGGER IF EXISTS messages_on_insert;
DROP TRIGGER IF EXISTS messages_on_delete;
DROP TRIGGER IF EXISTS messages_on_update;
DROP TABLE IF EXISTS ${MESSAGES_FTS_TABLE};
`);
writeLokiSchemaVersion(targetVersion, db);
})();
db.transaction(() => {
db.exec(`
-- Then we create our full-text search table and populate it
CREATE VIRTUAL TABLE ${MESSAGES_FTS_TABLE}
USING fts5(id UNINDEXED, body);
INSERT INTO ${MESSAGES_FTS_TABLE}(id, body)
SELECT id, body FROM ${MESSAGES_TABLE};
-- Then we set up triggers to keep the full-text search table up to date
CREATE TRIGGER messages_on_insert AFTER INSERT ON ${MESSAGES_TABLE} BEGIN
INSERT INTO ${MESSAGES_FTS_TABLE} (
id,
body
) VALUES (
new.id,
new.body
);
END;
CREATE TRIGGER messages_on_delete AFTER DELETE ON ${MESSAGES_TABLE} BEGIN
DELETE FROM ${MESSAGES_FTS_TABLE} WHERE id = old.id;
END;
CREATE TRIGGER messages_on_update AFTER UPDATE ON ${MESSAGES_TABLE} BEGIN
DELETE FROM ${MESSAGES_FTS_TABLE} WHERE id = old.id;
INSERT INTO ${MESSAGES_FTS_TABLE}(
id,
body
) VALUES (
new.id,
new.body
);
END;
`);
writeLokiSchemaVersion(targetVersion, db);
})();
console.log(`updateToLokiSchemaVersion${targetVersion}: success!`);
}
function updateToLokiSchemaVersion19(currentVersion, db) {
const targetVersion = 19;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
DROP INDEX messages_schemaVersion;
ALTER TABLE ${MESSAGES_TABLE} DROP COLUMN schemaVersion;
`);
// this is way to slow for now...
// db.exec(`
// UPDATE ${MESSAGES_TABLE} SET
// json = json_remove(json, '$.schemaVersion')
// `);
writeLokiSchemaVersion(targetVersion, db);
})();
console.log(`updateToLokiSchemaVersion${targetVersion}: success!`);
}
function writeLokiSchemaVersion(newVersion, db) {
db.prepare(
`INSERT INTO loki_schema(
@ -1841,7 +1925,6 @@ function saveMessage(data) {
serverTimestamp,
// eslint-disable-next-line camelcase
received_at,
schemaVersion,
sent,
// eslint-disable-next-line camelcase
sent_at,
@ -1876,7 +1959,6 @@ function saveMessage(data) {
hasFileAttachments,
hasVisualMediaAttachments,
received_at,
schemaVersion,
sent,
sent_at,
source,
@ -1901,7 +1983,6 @@ function saveMessage(data) {
hasFileAttachments,
hasVisualMediaAttachments,
received_at,
schemaVersion,
sent,
sent_at,
source,
@ -1922,7 +2003,6 @@ function saveMessage(data) {
$hasFileAttachments,
$hasVisualMediaAttachments,
$received_at,
$schemaVersion,
$sent,
$sent_at,
$source,
@ -2220,6 +2300,135 @@ function getFirstUnreadMessageIdInConversation(conversationId) {
return rows[0].id;
}
/**
* Deletes all but the 10,000 last received messages.
*/
function trimMessages(limit) {
console.log(limit); // adding this for linting purposes.
// METHOD 1 Start - Seems to take to long and freeze
// const convoCount = globalInstance
// .prepare(
// `
// SELECT COUNT(*) FROM ${MESSAGES_TABLE}
// WHERE conversationId = $conversationId
// `
// )
// .all({
// conversationId,
// });
// if (convoCount < limit) {
// console.log(`Skipping conversation: ${conversationId}`);
// return;
// } else {
// console.count('convo surpassed limit');
// }
// globalInstance
// .prepare(
// `
// DELETE FROM ${MESSAGES_TABLE}
// WHERE conversationId = $conversationId
// AND id NOT IN (
// SELECT id FROM ${MESSAGES_TABLE}
// WHERE conversationId = $conversationId
// ORDER BY received_at DESC
// LIMIT $limit
// );
// `
// )
// .run({
// conversationId,
// limit,
// });
// METHOD 1 END
// METHOD 2 Start
// const messages = globalInstance
// .prepare(
// `
// SELECT id, conversationId FROM ${MESSAGES_TABLE}
// CREATE VIRTUAL TABLE IF NOT EXISTS temp_deletion
// id STRING PRIMARY KEY ASC
// `
// )
// .all();
// const idsToDelete = [];
// const convoCountLookup = {};
// for (let index = 0; index < messages.length; index + 1) {
// const { conversationId, id } = messages[index];
// console.log(`run ${index} - convoId: ${conversationId}, messageId: ${id}`);
// if (!convoCountLookup[conversationId]) {
// convoCountLookup[conversationId] = 1;
// } else {
// convoCountLookup[conversationId] + 1;
// if (convoCountLookup[conversationId] > limit) {
// idsToDelete.push(id);
// }
// }
// }
// // Ideally should be able to do WHERE id IN (x, y, z) with an array of IDs
// // the array might need to be chunked as well for performance
// const idSlice = [...idsToDelete].slice(0, 30);
// idSlice.forEach(() => {
// globalInstance
// .prepare(
// `
// DELETE FROM ${MESSAGES_TABLE}
// WHERE id = $idSlice
// `
// )
// .run({
// idSlice,
// });
// });
// Method 2 End
// Method 3 start - Audric's suggestion
// const largeConvos = globalInstance
// .prepare(
// `
// SELECT conversationId, count(id) FROM ${MESSAGES_TABLE}
// GROUP BY conversationId
// HAVING COUNT(id) > 1000
// `
// )
// .all();
// console.log({ largeConvos });
// // finding 1000th msg timestamp
// largeConvos.forEach(convo => {
// const convoId = convo.conversationId;
// console.log({ convoId });
// const lastMsg = globalInstance
// .prepare(
// `
// SELECT received_at, sent_at FROM ${MESSAGES_TABLE}
// WHERE conversationId = $convoId
// ORDER BY received_at DESC
// LIMIT 1
// OFFSET 999
// `
// )
// .all({
// convoId,
// });
// // use timestamp with lesserThan as conditional for deletion
// console.log({ lastMsg });
// const timestamp = lastMsg[0].received_at;
// if (timestamp) {
// console.log({ timestamp, convoId });
// globalInstance
// .prepare(
// `
// DELETE FROM ${MESSAGES_TABLE}
// WHERE conversationId = $convoId
// AND received_at < $timestamp
// `
// )
// .run({
// timestamp,
// convoId,
// });
// }
// });
}
function getMessagesBySentAt(sentAt) {
const rows = globalInstance
.prepare(
@ -2921,3 +3130,139 @@ function removeOneOpenGroupV1Message() {
return toRemoveCount - 1;
}
/**
* Only using this for development. Populate conversation and message tables.
* @param {*} numConvosToAdd
* @param {*} numMsgsToAdd
*/
function fillWithTestData(numConvosToAdd, numMsgsToAdd) {
const convoBeforeCount = globalInstance
.prepare(`SELECT count(*) from ${CONVERSATIONS_TABLE};`)
.get()['count(*)'];
const lipsum =
// eslint:disable-next-line max-line-length
`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis ac ornare lorem,
non suscipit purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse cursus aliquet velit a dignissim. Integer at nisi sed velit consequat
dictum. Phasellus congue tellus ante. Ut rutrum hendrerit dapibus. Fusce
luctus, ante nec interdum molestie, purus urna volutpat turpis, eget mattis
lectus velit at velit. Praesent vel tellus turpis. Praesent eget purus at
nisl blandit pharetra. Cras dapibus sem vitae rutrum dapibus. Vivamus vitae mi
ante. Donec aliquam porta nibh, vel scelerisque orci condimentum sed.
Proin in mattis ipsum, ac euismod sem. Donec malesuada sem nisl, at
vehicula ante efficitur sed. Curabitur in sapien eros. Morbi tempor ante ut
metus scelerisque condimentum. Integer sit amet tempus nulla. Vivamus
imperdiet dui ac luctus vulputate. Sed a accumsan risus. Nulla facilisi.
Nulla mauris dui, luctus in sagittis at, sodales id mauris. Integer efficitur
viverra ex, ut dignissim eros tincidunt placerat. Sed facilisis gravida
mauris in luctus . Fusce dapibus, est vitae tincidunt eleifend, justo
odio porta dui, sed ultrices mi arcu vitae ante. Mauris ut libero
erat. Nam ut mi quis ante tincidunt facilisis sit amet id enim.
Vestibulum in molestie mi. In ac felis est. Vestibulum vel blandit ex. Morbi vitae
viverra augue . Ut turpis quam, cursus quis ex a, convallis
ullamcorper purus. Nam eget libero arcu. Integer fermentum enim nunc, non consequat urna
fermentum condimentum. Nulla vitae malesuada est. Donec imperdiet tortor interdum
malesuada feugiat. Integer pulvinar dui ex, eget tristique arcu mattis at. Nam eu neque
eget mauris varius suscipit. Quisque ac enim vitae mauris laoreet congue nec sed
justo. Curabitur fermentum quam eget est tincidunt, at faucibus lacus maximus. Donec
auctor enim dolor, faucibus egestas diam consectetur sed. Donec eget rutrum arcu, at
tempus mi. Fusce quis volutpat sapien. In aliquet fringilla purus. Ut eu nunc non
augue lacinia ultrices at eget tortor. Maecenas pulvinar odio sit amet purus
elementum, a vehicula lorem maximus. Pellentesque eu lorem magna. Vestibulum ut facilisis
lorem. Proin et enim cursus, vulputate neque sit amet, posuere enim. Praesent
faucibus tellus vel mi tincidunt, nec malesuada nibh malesuada. In laoreet sapien vitae
aliquet sollicitudin.
`;
const msgBeforeCount = globalInstance.prepare(`SELECT count(*) from ${MESSAGES_TABLE};`).get()[
'count(*)'
];
console.warn('==== fillWithTestData ====');
console.warn({
convoBeforeCount,
msgBeforeCount,
convoToAdd: numConvosToAdd,
msgToAdd: numMsgsToAdd,
});
const convosIdsAdded = [];
// eslint-disable-next-line no-plusplus
for (let index = 0; index < numConvosToAdd; index++) {
const activeAt = Date.now() - index;
const id = Date.now() - 1000 * index;
const convoObjToAdd = {
active_at: activeAt,
members: [],
profileName: `${activeAt}`,
name: `${activeAt}`,
id: `05${id}`,
type: 'group',
};
convosIdsAdded.push(id);
try {
saveConversation(convoObjToAdd);
// eslint-disable-next-line no-empty
} catch (e) {}
}
console.warn('convosIdsAdded', convosIdsAdded);
// eslint-disable-next-line no-plusplus
for (let index = 0; index < numMsgsToAdd; index++) {
const activeAt = Date.now() - index;
const id = Date.now() - 1000 * index;
const lipsumStartIdx = Math.floor(Math.random() * lipsum.length);
const lipsumLength = Math.floor(Math.random() * lipsum.length - lipsumStartIdx);
const fakeBodyText = lipsum.substring(lipsumStartIdx, lipsumStartIdx + lipsumLength);
const convoId = convosIdsAdded[Math.floor(Math.random() * convosIdsAdded.length)];
const msgObjToAdd = {
// body: `fake body ${activeAt}`,
body: `fakeMsgIdx-spongebob-${index} ${fakeBodyText} ${activeAt}`,
conversationId: `${convoId}`,
// eslint-disable-next-line camelcase
expires_at: 0,
hasAttachments: 0,
hasFileAttachments: 0,
hasVisualMediaAttachments: 0,
id: `${id}`,
serverId: 0,
serverTimestamp: 0,
// eslint-disable-next-line camelcase
received_at: Date.now(),
sent: 0,
// eslint-disable-next-line camelcase
sent_at: Date.now(),
source: `${convoId}`,
sourceDevice: 1,
type: '%',
unread: 1,
expireTimer: 0,
expirationStartTimestamp: 0,
};
if (convoId % 10 === 0) {
console.info('uyo , convoId ', { index, convoId });
}
try {
saveMessage(msgObjToAdd);
// eslint-disable-next-line no-empty
} catch (e) {
console.warn(e);
}
}
const convoAfterCount = globalInstance
.prepare(`SELECT count(*) from ${CONVERSATIONS_TABLE};`)
.get()['count(*)'];
const msgAfterCount = globalInstance.prepare(`SELECT count(*) from ${MESSAGES_TABLE};`).get()[
'count(*)'
];
console.warn({ convoAfterCount, msgAfterCount });
return convosIdsAdded;
}

@ -84,6 +84,7 @@
"jquery": "3.3.1",
"jsbn": "1.1.0",
"libsodium-wrappers": "^0.7.8",
"libxmljs": "^0.19.7",
"linkify-it": "3.0.2",
"lodash": "4.17.11",
"long": "^4.0.0",
@ -174,7 +175,7 @@
"@types/webpack": "^5.28.0",
"arraybuffer-loader": "1.0.3",
"asar": "0.14.0",
"chai": "4.3.4",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-bytes": "^0.1.2",
"css-loader": "^3.6.0",
@ -268,7 +269,11 @@
"StartupWMClass": "Session"
},
"asarUnpack": "node_modules/spellchecker/vendor/hunspell_dictionaries",
"target": ["deb", "rpm", "freebsd"],
"target": [
"deb",
"rpm",
"freebsd"
],
"icon": "build/icon.icns"
},
"asarUnpack": [

@ -0,0 +1,15 @@
const config = {
timeout: 300000,
globalTimeout: 6000000,
reporter: 'list',
testDir: './ts/test/automation',
testIgnore: '*.js',
outputDir: './ts/test/automation/test-results',
use: {
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
workers: 1,
};
module.exports = config;

@ -1455,7 +1455,7 @@
.module-search-results {
overflow-y: auto;
max-height: 100%;
color: white;
color: var(--color-text);
}
.module-search-results__conversations-header {
@ -1547,7 +1547,7 @@
white-space: nowrap;
text-overflow: ellipsis;
color: $color-gray-90;
color: var(--color-text);
}
.module-message-search-result__header__timestamp {
@ -1581,7 +1581,7 @@
font-size: 13px;
color: $color-gray-60;
color: var(--color-text-subtle);
max-height: 3.6em;

@ -0,0 +1,81 @@
import React from 'react';
import { RenderTextCallbackType } from '../../types/Util';
import { SizeClassType } from '../../util/emoji';
import { AddNewLines } from '../conversation/AddNewLines';
import { Emojify } from '../conversation/Emojify';
import { MessageBody } from '../conversation/message/message-content/MessageBody';
const renderNewLines: RenderTextCallbackType = ({ text, key }) => (
<AddNewLines key={key} text={text} />
);
const renderEmoji = ({
text,
key,
sizeClass,
renderNonEmoji,
}: {
text: string;
key: number;
sizeClass?: SizeClassType;
renderNonEmoji: RenderTextCallbackType;
}) => <Emojify key={key} text={text} sizeClass={sizeClass} renderNonEmoji={renderNonEmoji} />;
export const MessageBodyHighlight = (props: { text: string }) => {
const { text } = props;
const results: Array<JSX.Element> = [];
const FIND_BEGIN_END = /<<left>>(.+?)<<right>>/g;
let match = FIND_BEGIN_END.exec(text);
let last = 0;
let count = 1;
if (!match) {
return <MessageBody disableJumbomoji={true} disableLinks={true} text={text} />;
}
const sizeClass = '';
while (match) {
if (last < match.index) {
const beforeText = text.slice(last, match.index);
results.push(
renderEmoji({
text: beforeText,
sizeClass,
key: count++,
renderNonEmoji: renderNewLines,
})
);
}
const [, toHighlight] = match;
results.push(
<span className="module-message-body__highlight" key={count++}>
{renderEmoji({
text: toHighlight,
sizeClass,
key: count++,
renderNonEmoji: renderNewLines,
})}
</span>
);
// @ts-ignore
last = FIND_BEGIN_END.lastIndex;
match = FIND_BEGIN_END.exec(text);
}
if (last < text.length) {
results.push(
renderEmoji({
text: text.slice(last),
sizeClass,
key: count++,
renderNonEmoji: renderNewLines,
})
);
}
return <>{results}</>;
};

@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { Emojify } from './Emojify';
import { useConversationUsername } from '../../hooks/useParamSelector';
type Props = {
pubkey: string;
@ -17,13 +18,15 @@ export const ContactName = (props: Props) => {
const { pubkey, name, profileName, module, boldProfileName, compact, shouldShowPubkey } = props;
const prefix = module ? module : 'module-contact-name';
const shouldShowProfile = Boolean(profileName || name);
const convoName = useConversationUsername(pubkey);
const shouldShowProfile = Boolean(convoName || profileName || name);
const styles = (boldProfileName
? {
fontWeight: 'bold',
}
: {}) as React.CSSProperties;
const textProfile = profileName || name || window.i18n('anonymous');
const textProfile = profileName || name || convoName || window.i18n('anonymous');
const profileElement = shouldShowProfile ? (
<span style={styles as any} className={`${prefix}__profile-name`}>
<Emojify text={textProfile} />

@ -166,6 +166,7 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
className="messages-container"
onScroll={this.handleScroll}
ref={this.props.messageContainerRef}
data-testid="messages-container"
>
<UnreadAboveIndicator />

@ -384,6 +384,7 @@ class CompositionBoxInner extends React.Component<Props, State> {
ref={el => {
this.container = el;
}}
data-testid="message-input"
>
{this.renderTextArea()}
</div>

@ -54,6 +54,7 @@ export const SendMessageButton = (props: { onClick: () => void }) => {
borderRadius="300px"
iconPadding="6px"
onClick={props.onClick}
dataTestId="send-message-button"
/>
</div>
);

@ -25,6 +25,7 @@ type Props = {
onQuoteClick: (quote: QuoteClickOptions) => void;
ctxMenuID: string;
isDetailView?: boolean;
dataTestId?: string;
};
export const MessageContentWithStatuses = (props: Props) => {
@ -64,7 +65,7 @@ export const MessageContentWithStatuses = (props: Props) => {
}
};
const { messageId, onQuoteClick, ctxMenuID, isDetailView } = props;
const { messageId, onQuoteClick, ctxMenuID, isDetailView, dataTestId } = props;
if (!contentProps) {
return null;
}
@ -78,8 +79,13 @@ export const MessageContentWithStatuses = (props: Props) => {
onClick={onClickOnMessageOuterContainer}
onDoubleClickCapture={onDoubleClickReplyToMessage}
style={{ width: hasAttachments && isTrustedForAttachmentDownload ? 'min-content' : 'auto' }}
data-testid={dataTestId}
>
<MessageStatus messageId={messageId} isCorrectSide={isIncoming} />
<MessageStatus
dataTestId="msg-status-incoming"
messageId={messageId}
isCorrectSide={isIncoming}
/>
<div>
<MessageAuthorText messageId={messageId} />
@ -89,7 +95,11 @@ export const MessageContentWithStatuses = (props: Props) => {
onQuoteClick={onQuoteClick}
/>
</div>
<MessageStatus messageId={messageId} isCorrectSide={!isIncoming} />
<MessageStatus
dataTestId="msg-status-outgoing"
messageId={messageId}
isCorrectSide={!isIncoming}
/>
{!isDeleted && <MessageContextMenu messageId={messageId} contextMenuId={ctxMenuID} />}
</div>
);

@ -7,12 +7,13 @@ import { OutgoingMessageStatus } from './OutgoingMessageStatus';
type Props = {
isCorrectSide: boolean;
messageId: string;
dataTestId?: string;
};
export type MessageStatusSelectorProps = Pick<MessageRenderingProps, 'direction' | 'status'>;
export const MessageStatus = (props: Props) => {
const { isCorrectSide } = props;
const { isCorrectSide, dataTestId } = props;
const selected = useSelector(state => getMessageStatusProps(state as any, props.messageId));
if (!selected) {
@ -30,5 +31,5 @@ export const MessageStatus = (props: Props) => {
return null;
}
return <OutgoingMessageStatus status={status} />;
return <OutgoingMessageStatus dataTestId={dataTestId} status={status} />;
};

@ -12,57 +12,66 @@ const MessageStatusSendingContainer = styled.div`
cursor: pointer;
`;
const MessageStatusSending = () => {
const MessageStatusSending = ({ dataTestId }: { dataTestId?: string }) => {
const iconColor = 'var(--color-text)';
return (
<MessageStatusSendingContainer>
<MessageStatusSendingContainer data-testid={dataTestId} data-test-type="sending">
<SessionIcon rotateDuration={2} iconColor={iconColor} iconType="sending" iconSize="tiny" />
</MessageStatusSendingContainer>
);
};
const MessageStatusSent = () => {
const MessageStatusSent = ({ dataTestId }: { dataTestId?: string }) => {
const iconColor = 'var(--color-text)';
return (
<MessageStatusSendingContainer>
<MessageStatusSendingContainer data-testid={dataTestId} data-test-type="sent">
<SessionIcon iconColor={iconColor} iconType="circleCheck" iconSize="tiny" />
</MessageStatusSendingContainer>
);
};
const MessageStatusRead = () => {
const MessageStatusRead = ({ dataTestId }: { dataTestId?: string }) => {
const iconColor = 'var(--color-text)';
return (
<MessageStatusSendingContainer>
<MessageStatusSendingContainer data-testid={dataTestId} data-test-type="read">
<SessionIcon iconColor={iconColor} iconType="doubleCheckCircleFilled" iconSize="tiny" />
</MessageStatusSendingContainer>
);
};
const MessageStatusError = () => {
const MessageStatusError = ({ dataTestId }: { dataTestId?: string }) => {
const showDebugLog = () => {
ipcRenderer.send('show-debug-log');
};
return (
<MessageStatusSendingContainer onClick={showDebugLog} title={window.i18n('sendFailed')}>
<MessageStatusSendingContainer
data-testid={dataTestId}
data-test-type="failed"
onClick={showDebugLog}
title={window.i18n('sendFailed')}
>
<SessionIcon iconColor={'var(--color-destructive'} iconType="error" iconSize="tiny" />
</MessageStatusSendingContainer>
);
};
export const OutgoingMessageStatus = (props: { status?: MessageDeliveryStatus | null }) => {
switch (props.status) {
export const OutgoingMessageStatus = (props: {
status?: MessageDeliveryStatus | null;
dataTestId?: string;
}) => {
const { status, dataTestId } = props;
switch (status) {
case 'sending':
return <MessageStatusSending />;
return <MessageStatusSending dataTestId={dataTestId} />;
case 'sent':
return <MessageStatusSent />;
return <MessageStatusSent dataTestId={dataTestId} />;
case 'read':
return <MessageStatusRead />;
return <MessageStatusRead dataTestId={dataTestId} />;
case 'error':
return <MessageStatusError />;
return <MessageStatusError dataTestId={dataTestId} />;
default:
return null;
}

@ -181,6 +181,7 @@ export const GenericReadableMessage = (props: Props) => {
messageId={messageId}
onQuoteClick={props.onQuoteClick}
isDetailView={isDetailView}
dataTestId={`message-content-${messageId}`}
/>
<ExpireTimer
isCorrectSide={isIncoming}

@ -43,7 +43,7 @@ export const LeftPaneSectionHeader = (props: { buttonClicked?: any }) => {
<div className="module-left-pane__header">
<SectionTitle>{label}</SectionTitle>
{isMessageSection && (
<SessionButton onClick={props.buttonClicked}>
<SessionButton onClick={props.buttonClicked} dataTestId="new-conversation-button">
<SessionIcon iconType="plus" iconSize="small" iconColor="white" />
</SessionButton>
)}

@ -114,6 +114,7 @@ export const OverlayClosedGroup = () => {
text={buttonText}
disabled={noContactsForClosedGroup}
onClick={onEnterPressed}
dataTestId="next-button"
/>
</div>
);

@ -55,6 +55,7 @@ const ContinueYourSessionButton = (props: {
buttonColor={SessionButtonColor.Green}
text={window.i18n('continueYourSession')}
disabled={props.disabled}
dataTestId="continue-session-button;"
/>
);
};

@ -0,0 +1,146 @@
import React from 'react';
import classNames from 'classnames';
import { MessageDirection } from '../../models/messageType';
import { getOurPubKeyStrFromCache } from '../../session/utils/User';
import {
FindAndFormatContactType,
openConversationWithMessages,
} 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';
type PropsHousekeeping = {
isSelected?: boolean;
};
export type PropsForSearchResults = {
from: FindAndFormatContactType;
to: FindAndFormatContactType;
id: string;
conversationId: string;
destination: string;
source: string;
direction?: string;
snippet?: string; //not sure about the type of snippet
receivedAt?: number;
};
export type MessageResultProps = PropsForSearchResults & PropsHousekeeping;
const FromName = (props: { source: string; destination: string }) => {
const { source, destination } = props;
const isNoteToSelf = destination === getOurPubKeyStrFromCache() && source === destination;
if (isNoteToSelf) {
return (
<span className="module-message-search-result__header__name">
{window.i18n('noteToSelf')}
</span>
);
}
if (source === getOurPubKeyStrFromCache()) {
return <span className="module-message-search-result__header__name">{window.i18n('you')}</span>;
}
return (
// tslint:disable: use-simple-attributes
<ContactName
pubkey={source}
module="module-message-search-result__header__name"
shouldShowPubkey={false}
/>
);
};
const From = (props: { source: string; destination: string }) => {
const { source, destination } = props;
const fromName = <FromName source={source} destination={destination} />;
const ourKey = getOurPubKeyStrFromCache();
if (destination !== ourKey) {
return (
<div className="module-message-search-result__header__from">
{fromName} {window.i18n('to')}
<span className="module-mesages-search-result__header__group">
<ContactName pubkey={destination} shouldShowPubkey={false} />
</span>
</div>
);
}
return <div className="module-message-search-result__header__from">{fromName}</div>;
};
const AvatarItem = (props: { source: string }) => {
const { source } = props;
return <Avatar size={AvatarSize.S} pubkey={source} />;
};
export const MessageSearchResult = (props: MessageResultProps) => {
const {
isSelected,
id,
conversationId,
receivedAt,
snippet,
destination,
source,
direction,
} = props;
// Some messages miss a source or destination. Doing checks to see if the fields can be derived from other sources.
// E.g. if the source is missing but the message is outgoing, the source will be our pubkey
const sourceOrDestinationDerivable =
(destination && direction === MessageDirection.outgoing) ||
!destination ||
!source ||
(source && direction === MessageDirection.incoming);
if (!sourceOrDestinationDerivable) {
return null;
}
const effectiveSource =
!source && direction === MessageDirection.outgoing ? getOurPubKeyStrFromCache() : source;
const effectiveDestination =
!destination && direction === MessageDirection.incoming
? getOurPubKeyStrFromCache()
: destination;
return (
<div
key={`div-msg-searchresult-${id}`}
role="button"
onClick={async () => {
await openConversationWithMessages({
conversationKey: conversationId,
messageId: id,
});
}}
className={classNames(
'module-message-search-result',
isSelected ? 'module-message-search-result--is-selected' : null
)}
>
<AvatarItem source={effectiveSource} />
<div className="module-message-search-result__text">
<div className="module-message-search-result__header">
<From source={effectiveSource} destination={effectiveDestination} />
<div className="module-message-search-result__header__timestamp">
<Timestamp timestamp={receivedAt} />
</div>
</div>
<div className="module-message-search-result__body">
<MessageBodyHighlight text={snippet || ''} />
</div>
</div>
</div>
);
};

@ -3,10 +3,12 @@ import {
ConversationListItemProps,
MemoConversationListItemWithDetails,
} from '../leftpane/conversation-list-item/ConversationListItem';
import { MessageResultProps, MessageSearchResult } from './MessageSearchResults';
export type SearchResultsProps = {
contacts: Array<ConversationListItemProps>;
conversations: Array<ConversationListItemProps>;
messages: Array<MessageResultProps>;
hideMessagesHeader: boolean;
searchTerm: string;
};
@ -23,11 +25,12 @@ const ContactsItem = (props: { header: string; items: Array<ConversationListItem
};
export const SearchResults = (props: SearchResultsProps) => {
const { conversations, contacts, searchTerm } = props;
const { conversations, contacts, messages, searchTerm, hideMessagesHeader } = props;
const haveConversations = conversations && conversations.length;
const haveContacts = contacts && contacts.length;
const noResults = !haveConversations && !haveContacts;
const haveMessages = messages && messages.length;
const noResults = !haveConversations && !haveContacts && !haveMessages;
return (
<div className="module-search-results">
@ -50,7 +53,7 @@ export const SearchResults = (props: SearchResultsProps) => {
<ContactsItem header={window.i18n('contactsHeader')} items={contacts} />
) : null}
{/* {haveMessages ? (
{haveMessages ? (
<div className="module-search-results__messages">
{hideMessagesHeader ? null : (
<div className="module-search-results__messages-header">
@ -58,10 +61,10 @@ export const SearchResults = (props: SearchResultsProps) => {
</div>
)}
{messages.map(message => (
<MessageSearchResult key={message.id} {...message} />
<MessageSearchResult key={`search-result-${message.id}`} {...message} />
))}
</div>
) : null} */}
) : null}
</div>
);
};

@ -3,7 +3,11 @@ import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
// tslint:disable-next-line: no-submodule-imports
import useUpdate from 'react-use/lib/useUpdate';
import { createOrUpdateItem, hasLinkPreviewPopupBeenDisplayed } from '../../../data/data';
import {
createOrUpdateItem,
fillWithTestData,
hasLinkPreviewPopupBeenDisplayed,
} from '../../../data/data';
import { ToastUtils } from '../../../session/utils';
import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import { toggleAudioAutoplay } from '../../../state/ducks/userConfig';
@ -131,6 +135,28 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null
buttonColor={SessionButtonColor.Primary}
buttonText={window.i18n('translation')}
/>
{/* <SessionSettingButtonItem
title={window.i18n('trimDatabase')}
description={window.i18n('trimDatabaseDescription')}
onClick={async () => {
const msgCount = await getMessageCount();
const deleteAmount = Math.max(msgCount - 10000, 0);
dispatch(
updateConfirmModal({
onClickOk: () => {
void trimMessages();
},
onClickClose: () => {
updateConfirmModal(null);
},
message: window.i18n('trimDatabaseConfirmationBody', [`${deleteAmount}`]),
})
);
}}
buttonColor={SessionButtonColor.Primary}
buttonText={window.i18n('trimDatabase')}
/> */}
<SessionSettingButtonItem
onClick={() => {
ipcRenderer.send('show-debug-log');
@ -138,6 +164,13 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null
buttonColor={SessionButtonColor.Primary}
buttonText={window.i18n('showDebugLog')}
/>
<SessionSettingButtonItem
onClick={async () => {
await fillWithTestData(100, 1000);
}}
buttonColor={SessionButtonColor.Primary}
buttonText={'Spam fill DB using cached'}
/>
</>
);
}

@ -3,10 +3,15 @@ import { ipcRenderer } from 'electron';
// tslint:disable: no-require-imports no-var-requires one-variable-per-declaration no-void-expression
import _ from 'lodash';
import { ConversationCollection, ConversationModel } from '../models/conversation';
import {
ConversationCollection,
ConversationModel,
ConversationTypeEnum,
} from '../models/conversation';
import { MessageCollection, MessageModel } from '../models/message';
import { MessageAttributes } from '../models/messageType';
import { MessageAttributes, MessageDirection } from '../models/messageType';
import { HexKeyPair } from '../receiver/keypairs';
import { getConversationController } from '../session/conversations';
import { getSodium } from '../session/crypto';
import { PubKey } from '../session/types';
import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String';
@ -109,6 +114,7 @@ const channelsToMake = {
removeAllMessagesInConversation,
getMessageCount,
getMessageBySender,
getMessageBySenderAndServerTimestamp,
getMessageBySenderAndTimestamp,
@ -123,6 +129,7 @@ const channelsToMake = {
hasConversationOutgoingMessage,
getSeenMessagesByHashList,
getLastHashBySnode,
trimMessages,
getUnprocessedCount,
getAllUnprocessed,
@ -156,6 +163,9 @@ const channelsToMake = {
removeAllClosedGroupEncryptionKeyPairs,
removeOneOpenGroupV1Message,
// dev performance testing
fillWithTestData,
// open group v2
...channelstoMakeOpenGroupV2,
};
@ -191,8 +201,11 @@ export function init() {
});
}
// When IPC arguments are prepared for the cross-process send, they are JSON.stringified.
// We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates).
/**
* When IPC arguments are prepared for the cross-process send, they are JSON.stringified.
* We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates).
* @param data - data to be cleaned
*/
function _cleanData(data: any): any {
const keys = Object.keys(data);
@ -575,6 +588,7 @@ export async function searchConversations(query: string): Promise<Array<any>> {
export async function searchMessages(query: string, { limit }: any = {}): Promise<Array<any>> {
const messages = await channels.searchMessages(query, { limit });
console.warn('searched message', messages);
return messages;
}
@ -758,6 +772,13 @@ export async function getMessagesByConversation(
return new MessageCollection(messages);
}
/**
* @returns Returns count of all messages in the database
*/
export async function getMessageCount() {
return channels.getMessageCount();
}
export async function getFirstUnreadMessageIdInConversation(
conversationId: string
): Promise<string | undefined> {
@ -801,6 +822,11 @@ export async function removeAllMessagesInConversation(conversationId: string): P
} while (messages.length > 0);
}
export async function trimMessages(): Promise<void> {
await channels.trimMessages(1000);
return;
}
export async function getMessagesBySentAt(sentAt: number): Promise<MessageCollection> {
const messages = await channels.getMessagesBySentAt(sentAt);
return new MessageCollection(messages);
@ -964,3 +990,36 @@ export async function updateSnodePoolOnDb(snodesAsJsonString: string): Promise<v
export async function removeOneOpenGroupV1Message(): Promise<number> {
return channels.removeOneOpenGroupV1Message();
}
/**
* Generates fake conversations and distributes messages amongst the conversations randomly
* @param numConvosToAdd Amount of fake conversations to generate
* @param numMsgsToAdd Number of fake messages to generate
*/
export async function fillWithTestData(convs: number, msgs: number) {
const newConvos = [];
for (let convsAddedCount = 0; convsAddedCount < convs; convsAddedCount++) {
const convoId = `${Date.now()} + ${convsAddedCount}`;
const newConvo = await getConversationController().getOrCreateAndWait(
convoId,
ConversationTypeEnum.PRIVATE
);
newConvos.push(newConvo);
}
for (let msgsAddedCount = 0; msgsAddedCount < msgs; msgsAddedCount++) {
if (msgsAddedCount % 100 === 0) {
console.warn(msgsAddedCount);
}
// tslint:disable: insecure-random
const convoToChoose = newConvos[Math.floor(Math.random() * newConvos.length)];
await convoToChoose.addSingleMessage({
source: convoToChoose.id,
type: MessageDirection.outgoing,
conversationId: convoToChoose.id,
body: `spongebob ${new Date().toString()}`,
// tslint:disable: insecure-random
direction: Math.random() > 0.5 ? 'outgoing' : 'incoming',
});
}
}

@ -23,6 +23,7 @@ import { toHex } from '../session/utils/String';
import {
actions as conversationActions,
conversationChanged,
conversationsChanged,
LastMessageStatusType,
MessageModelPropsWithoutConvoProps,
ReduxConversationType,
@ -204,10 +205,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
trailing: true,
leading: true,
});
this.triggerUIRefresh = _.throttle(this.triggerUIRefresh, 1000, {
trailing: true,
leading: true,
});
this.throttledNotify = _.debounce(this.notify, 500, { maxWait: 5000, trailing: true });
//start right away the function is called, and wait 1sec before calling it again
const markReadDebounced = _.debounce(this.markReadBouncy, 1000, {
@ -915,14 +913,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
public triggerUIRefresh() {
window.inboxStore?.dispatch(
conversationChanged({
id: this.id,
data: {
...this.getConversationModelProps(),
},
})
);
updatesToDispatch.set(this.id, this.getConversationModelProps());
trotthledAllConversationsDispatch();
}
public async commit() {
@ -1253,26 +1245,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
}
public async upgradeMessages(messages: any) {
// tslint:disable-next-line: one-variable-per-declaration
for (let max = messages.length, i = 0; i < max; i += 1) {
const message = messages.at(i);
const { attributes } = message;
const { schemaVersion } = attributes;
if (schemaVersion < window.Signal.Types.Message.VERSION_NEEDED_FOR_DISPLAY) {
// Yep, we really do want to wait for each of these
// eslint-disable-next-line no-await-in-loop
const { upgradeMessageSchema } = window.Signal.Migrations;
const upgradedMessage = await upgradeMessageSchema(attributes);
message.set(upgradedMessage);
// eslint-disable-next-line no-await-in-loop
await upgradedMessage.commit();
}
}
}
public hasMember(pubkey: string) {
return _.includes(this.get('members'), pubkey);
}
@ -1665,6 +1637,18 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
}
const trotthledAllConversationsDispatch = _.throttle(() => {
if (updatesToDispatch.size === 0) {
return;
}
console.warn('updatesToDispatch.size ', updatesToDispatch.size);
window.inboxStore?.dispatch(conversationsChanged([...updatesToDispatch.values()]));
updatesToDispatch.clear();
}, 500);
const updatesToDispatch: Map<string, ReduxConversationType> = new Map();
export class ConversationCollection extends Backbone.Collection<ConversationModel> {
constructor(models?: Array<ConversationModel>) {
super(models);

@ -75,13 +75,6 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
const filledAttrs = fillMessageAttributesWithDefaults(attributes);
super(filledAttrs);
this.set(
window.Signal.Types.Message.initializeSchemaVersion({
message: filledAttrs,
logger: window.log,
})
);
if (!this.attributes.id) {
throw new Error('A message always needs to have an id.');
}

@ -33,7 +33,6 @@ export interface MessageAttributes {
hasAttachments: boolean;
hasFileAttachments: boolean;
hasVisualMediaAttachments: boolean;
schemaVersion: number;
expirationTimerUpdate?: {
expireTimer: number;
source: string;
@ -118,6 +117,11 @@ export interface DataExtractionNotificationMsg {
referencedAttachmentTimestamp: number; // the attachment timestamp he screenshot
}
export enum MessageDirection {
outgoing = 'outgoing',
incoming = 'incoming',
}
export type PropsForDataExtractionNotification = DataExtractionNotificationMsg & {
name: string;
messageId: string;
@ -158,7 +162,6 @@ export interface MessageAttributesOptionals {
hasAttachments?: boolean;
hasFileAttachments?: boolean;
hasVisualMediaAttachments?: boolean;
schemaVersion?: number;
expirationTimerUpdate?: {
expireTimer: number;
source: string;
@ -199,7 +202,6 @@ export const fillMessageAttributesWithDefaults = (
const defaulted = _.defaults(optAttributes, {
expireTimer: 0, // disabled
id: uuidv4(),
schemaVersion: window.Signal.Types.Message.CURRENT_SCHEMA_VERSION,
unread: 0, // if nothing is set, this message is considered read
});
// this is just to cleanup a bit the db. delivered and delivered_to were removed, so everytime we load a message

@ -18,9 +18,6 @@ function contentTypeSupported(type: string): boolean {
// tslint:disable-next-line: cyclomatic-complexity
async function copyFromQuotedMessage(msg: MessageModel, quote?: Quote): Promise<void> {
const { upgradeMessageSchema } = window.Signal.Migrations;
const { Message: TypedMessage, Errors } = window.Signal.Types;
if (!quote) {
return;
}
@ -217,7 +214,6 @@ async function handleRegularMessage(
hasFileAttachments: dataMessage.hasFileAttachments,
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
quote: dataMessage.quote,
schemaVersion: dataMessage.schemaVersion,
attachments: dataMessage.attachments,
body: dataMessage.body,
conversationId: conversation.id,

@ -566,23 +566,25 @@ const conversationsSlice = createSlice({
}>
) {
const { payload } = action;
const { id, data } = payload;
const { conversationLookup, selectedConversation } = state;
return applyConversationChanged(state, payload);
},
conversationsChanged(
state: ConversationsStateType,
action: PayloadAction<Array<ReduxConversationType>>
) {
const { payload } = action;
const existing = conversationLookup[id];
// In the change case we only modify the lookup if we already had that conversation
if (!existing) {
return state;
let updatedState = state;
if (payload.length) {
payload.forEach(convoProps => {
updatedState = applyConversationChanged(updatedState, {
id: convoProps.id,
data: convoProps,
});
});
}
return {
...state,
selectedConversation,
conversationLookup: {
...conversationLookup,
[id]: data,
},
};
return updatedState;
},
conversationRemoved(state: ConversationsStateType, action: PayloadAction<string>) {
@ -783,12 +785,36 @@ const conversationsSlice = createSlice({
},
});
function applyConversationChanged(
state: ConversationsStateType,
payload: { id: string; data: ReduxConversationType }
) {
const { id, data } = payload;
const { conversationLookup, selectedConversation } = state;
const existing = conversationLookup[id];
// In the change case we only modify the lookup if we already had that conversation
if (!existing) {
return state;
}
return {
...state,
selectedConversation,
conversationLookup: {
...conversationLookup,
[id]: data,
},
};
}
// destructures
export const { actions, reducer } = conversationsSlice;
export const {
// conversation and messages list
conversationAdded,
conversationChanged,
conversationsChanged,
conversationRemoved,
removeAllConversations,
messageExpired,

@ -5,6 +5,7 @@ import { searchConversations, searchMessages } from '../../../ts/data/data';
import { ReduxConversationType } from './conversations';
import { PubKey } from '../../session/types';
import { ConversationTypeEnum } from '../../models/conversation';
import _ from 'lodash';
// State
@ -16,6 +17,10 @@ export type SearchStateType = {
// For conversations we store just the id, and pull conversation props in the selector
conversations: Array<string>;
contacts: Array<string>;
// TODO: ww typing
messages?: Array<any>;
messagesLookup?: any;
};
// Actions
@ -24,6 +29,8 @@ type SearchResultsPayloadType = {
normalizedPhoneNumber?: string;
conversations: Array<string>;
contacts: Array<string>;
messages?: Array<string>;
};
type SearchResultsKickoffActionType = {
@ -75,7 +82,7 @@ async function doSearch(query: string, options: SearchOptions): Promise<SearchRe
queryMessages(processedQuery),
]);
const { conversations, contacts } = discussions;
let filteredMessages = messages.filter(message => message !== undefined);
let filteredMessages = _.compact(messages);
if (isAdvancedQuery) {
let senderFilter: Array<string> = [];
@ -88,12 +95,12 @@ async function doSearch(query: string, options: SearchOptions): Promise<SearchRe
}
filteredMessages = filterMessages(filteredMessages, advancedSearchOptions, senderFilter);
}
return {
query,
normalizedPhoneNumber: PubKey.normalize(query),
conversations,
contacts,
messages: filteredMessages,
};
}
export function clearSearch(): ClearSearchActionType {
@ -194,7 +201,6 @@ function getAdvancedSearchOptionsFromQuery(query: string): AdvancedSearchOptions
async function queryMessages(query: string) {
try {
const normalized = cleanSearchTerm(query);
return searchMessages(normalized);
} catch (e) {
return [];
@ -247,6 +253,8 @@ export const initialSearchState: SearchStateType = {
query: '',
conversations: [],
contacts: [],
messages: [],
messagesLookup: {},
};
function getEmptyState(): SearchStateType {
@ -274,8 +282,7 @@ export function reducer(state: SearchStateType | undefined, action: SEARCH_TYPES
if (action.type === 'SEARCH_RESULTS_FULFILLED') {
const { payload } = action;
const { query, normalizedPhoneNumber, conversations, contacts } = payload;
const { query, normalizedPhoneNumber, conversations, contacts, messages } = payload;
// Reject if the associated query is not the most recent user-provided query
if (state.query !== query) {
return state;
@ -287,6 +294,7 @@ export function reducer(state: SearchStateType | undefined, action: SEARCH_TYPES
normalizedPhoneNumber,
conversations,
contacts,
messages,
};
}

@ -23,11 +23,17 @@ export const isSearching = createSelector(getSearch, (state: SearchStateType) =>
});
export const getSearchResults = createSelector(
[getSearch, getConversationLookup, getSelectedConversationKey],
(state: SearchStateType, lookup: ConversationLookupType, selectedConversation?: string) => {
[getSearch, getConversationLookup, getSelectedConversationKey, getSelectedMessage],
(
searchState: SearchStateType,
lookup: ConversationLookupType,
selectedConversation?: string,
selectedMessage?: string
) => {
console.warn({ state: searchState });
return {
contacts: compact(
state.contacts.map(id => {
searchState.contacts.map(id => {
const value = lookup[id];
if (value && id === selectedConversation) {
@ -41,7 +47,7 @@ export const getSearchResults = createSelector(
})
),
conversations: compact(
state.conversations.map(id => {
searchState.conversations.map(id => {
const value = lookup[id];
// Don't return anything when activeAt is unset (i.e. no current conversations with this user)
@ -60,9 +66,21 @@ export const getSearchResults = createSelector(
return value;
})
),
messages: compact(
searchState.messages?.map(message => {
if (message.id === selectedMessage) {
return {
...message,
isSelected: true,
};
}
return message;
})
),
hideMessagesHeader: false,
searchTerm: state.query,
searchTerm: searchState.query,
};
}
);

@ -0,0 +1,27 @@
import { _electron, Page, test } from '@playwright/test';
import { newUser } from './new_user';
import { openApp } from './open';
// Open app
let window: Page | undefined;
test('Check Password', async () => {
// open Electron
window = await openApp('1');
// Create user
await newUser(window, 'userA');
// Click on settings tab
await window.click('[data-testid=settings-section]');
// Click on privacy
await window.click('"Privacy"');
// Click set password
await window.click('"Set Password"');
// Enter password
await window.type('#password-modal-input', '123456');
// Confirm password
await window.type('#password-modal-input-confirm', '123456');
// Click OK
await window.keyboard.press('Enter');
// Type password into input field
await window.fill('#password-lock-input', '123456');
// Click OK
await window.click('"OK"');
});

@ -0,0 +1,12 @@
import test, { _electron, Page } from '@playwright/test';
import { getAppDataPath } from './open';
export const cleanUp = async (window: Page) => {
await window.click('[data-testid=settings-section]');
await window.click('text=Clear All Data');
await window.click('text=Entire Account');
await window.click('text=I am sure');
await window.waitForTimeout(10000);
};

@ -0,0 +1,48 @@
import { _electron, test } from '@playwright/test';
import { newUser } from './new_user';
import { openApp } from './open';
import { sendMessage } from './send_message';
const userADisplayName = 'userA';
const userBDisplayName = 'userB';
const userCDisplayName = 'userC';
const testMessage = 'Sending Test Message';
const testReply = 'Sending Reply Test Message';
test('Create group', async () => {
// Open Electron
const [windowA, windowB, windowC] = await Promise.all([openApp('1'), openApp('2'), openApp('3')]);
// Create User x3
// create userA
const userA = await newUser(windowA, userADisplayName);
// create userB
const userB = await newUser(windowB, userBDisplayName);
// Create UserC
const userC = await newUser(windowC, userCDisplayName);
// Add contact
await sendMessage(windowA, userB.sessionid, testMessage);
await sendMessage(windowB, userA.sessionid, testReply);
await sendMessage(windowA, userC.sessionid, testMessage);
await sendMessage(windowC, userA.sessionid, testReply);
// Create group with existing contact and session ID (of non-contact)
// Click new closed group tab
await windowA.click('"New Closed Group"');
// Enter group name
await windowA.fill('.session-id-editable', 'Test Group');
// Select user B
await windowA.click(userBDisplayName);
// Select user C
await windowA.click(userCDisplayName);
// Click Done
await windowA.click('"Done"');
// Check group was successfully created
windowA.locator(`text=${userBDisplayName}, ${userCDisplayName} + 'You joined the group'`);
// Send message in group chat from user a
await windowA.fill('[data-testid=message-input] * textarea', testMessage);
// Verify it was received by other two accounts
// Send message from user 2
// Verify
// Send message from user 3
// Verify
});

@ -0,0 +1,33 @@
import { _electron, expect, Page, test } from '@playwright/test';
import { newUser } from './new_user';
import { openApp } from './open';
import { sleepFor } from '../../session/utils/Promise';
// import {emptyDirSync} from 'fs-extra';
let window: Page | undefined;
test('Create User', async () => {
// Launch Electron app.
window = await openApp('1');
// Create User
const userA = await newUser(window, 'userA');
await window.click('[data-testid=leftpane-primary-avatar]');
await sleepFor(100);
//check username matches
expect(await window.innerText('[data-testid=your-profile-name]')).toBe(userA.userName);
//check session id matches
expect(await window.innerText('[data-testid=your-session-id]')).toBe(userA.sessionid);
// exit profile module
await window.click('.session-icon-button.small');
// go to settings section
await window.click('[data-testid=settings-section]');
await window.click('text=Recovery Phrase');
// check recovery phrase matches
expect(await window.innerText('[data-testid=recovery-phrase-seed-modal]')).toBe(
userA.recoveryPhrase
);
// Exit profile module
await window.click('.session-icon-button.small');
});

@ -0,0 +1,15 @@
import { _electron, Page } from '@playwright/test';
import { sleepFor } from '../../session/utils/Promise';
export const logIn = async (window: Page, userName: string, recoveryPhrase: string) => {
// restore account
await window.click('[data-testid=restore-using-recovery');
// Enter recovery phrase
await window.fill('[data-testid=recovery-phrase-input]', recoveryPhrase);
// Enter display name
await window.fill('[data-testid=display-name-input]', userName);
// Click continue your session
await window.click('[data-testid=continue-session-button]');
await sleepFor(100);
};

@ -0,0 +1,36 @@
import { _electron, expect, test } from '@playwright/test';
import { newUser } from './new_user';
import { openApp } from './open';
import { sendMessage } from './send_message';
const userADisplayName = 'userA';
const userBDisplayName = 'userB';
const timeStamp = Date.now();
const testMessage = 'Test-Message-';
const testReply = 'Sending Reply Test Message';
// Send message in one to one conversation with new contact
test('Send message to new contact', async () => {
const [windowA, windowB] = await Promise.all([openApp('1'), openApp('2')]);
// Create User A
const userA = await newUser(windowA, userADisplayName);
// Create User B
const userB = await newUser(windowB, userBDisplayName);
// User A sends message to User B
await sendMessage(windowA, userB.sessionid, `${testMessage} + ${timeStamp}`);
windowA.locator(`${testMessage} > svg`).waitFor;
await windowA.isVisible('[data-testid=msg-status-outgoing]');
await windowA.waitForTimeout(5500);
// User B sends message to User B to USER A
await sendMessage(windowB, userA.sessionid, `${testReply} + ${timeStamp}`);
await windowA.waitForTimeout(5500);
// Navigate to contacts tab in User B's window
await windowB.click('[data-testid=contact-section]');
await windowA.waitForTimeout(2500);
expect(await windowB.innerText('.module-conversation__user__profile-name')).toBe(userA.userName);
// Navigate to contacts tab in User A's window
await windowA.click('[data-testid=contact-section]');
expect(await windowA.innerText('.module-conversation__user__profile-name')).toBe(userB.userName);
});

@ -0,0 +1,20 @@
import { _electron, Page } from '@playwright/test';
export const newUser = async (window: Page, userName: string) => {
// Create User
await window.click('text=Create Session ID');
// Wait for animation for finish creating ID
await window.waitForTimeout(1500);
//Save session ID to a variable
const sessionid = await window.inputValue('[data-testid=session-id-signup]');
await window.click('text=Continue');
// Input username = testuser
await window.fill('#session-input-floating-label', userName);
await window.click('text=Get Started');
// save recovery phrase
await window.click('text=Reveal recovery phrase');
const recoveryPhrase = await window.innerText('[data-testid=recovery-phrase-seed-modal]');
await window.click('.session-icon-button.small');
return { userName, sessionid, recoveryPhrase };
};

@ -0,0 +1,63 @@
import test, { _electron } from '@playwright/test';
import { readdirSync, rmdirSync } from 'fs';
import * as path from 'path';
const NODE_ENV = 'test-integration';
let appDataPath: undefined | string;
test.beforeAll(async () => {
appDataPath = await getAppDataPath();
});
const getDirectoriesOfSessionDataPath = (source: string) =>
readdirSync(source, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name)
.filter(n => n.startsWith(`Session-${NODE_ENV}`));
test.beforeEach(() => {
if (!appDataPath || !appDataPath.length) {
throw new Error('appDataPath unset');
}
const parentFolderOfAllDataPath = path.dirname(appDataPath);
if (!parentFolderOfAllDataPath || parentFolderOfAllDataPath.length < 20) {
throw new Error('parentFolderOfAllDataPath not found or invalid');
}
const allAppDataPath = getDirectoriesOfSessionDataPath(parentFolderOfAllDataPath);
allAppDataPath.map(folder => {
if (!appDataPath) {
throw new Error('parentFolderOfAllDataPath unset');
}
const pathToRemove = path.join(parentFolderOfAllDataPath, folder);
console.warn('Removing old test data left at: ', pathToRemove);
rmdirSync(pathToRemove, { recursive: true });
});
});
export const getAppDataPath = async () => {
process.env.NODE_ENV = NODE_ENV;
const electronApp = await _electron.launch({ args: ['main.js'] });
const appPath = await electronApp.evaluate(async ({ app }) => {
return app.getPath('userData');
});
const window = await electronApp.firstWindow();
await window.close();
return appPath;
};
export const openApp = async (multi: string) => {
process.env.NODE_APP_INSTANCE = multi;
process.env.NODE_ENV = NODE_ENV;
const electronApp = await _electron.launch({ args: ['main.js'] });
// Get the first window that the app opens, wait if necessary.
const window = await electronApp.firstWindow();
await window.reload();
return window;
};

@ -0,0 +1,13 @@
import { _electron, Page } from '@playwright/test';
export const sendMessage = async (window: Page, sessionid: string, message: string) => {
await window.click('[data-testid=new-conversation-button]');
// Enter session ID of USER B
await window.fill('.session-id-editable-textarea', sessionid);
// click next
await window.click('text=Next');
// type into message input box
await window.fill('[data-testid=message-input] * textarea', message);
// click up arrow (send)
await window.click('[data-testid=send-message-button]');
};

@ -268,15 +268,6 @@ export type Attachment = {
contentType?: MIME.MIMEType;
size?: number;
data: ArrayBuffer;
// // Omit unused / deprecated keys:
// schemaVersion?: number;
// id?: string;
// width?: number;
// height?: number;
// thumbnail?: ArrayBuffer;
// key?: ArrayBuffer;
// digest?: ArrayBuffer;
} & Partial<AttachmentSchemaVersion3>;
interface AttachmentSchemaVersion3 {

@ -462,4 +462,7 @@ export type LocalizerKeys =
| 'searchFor...'
| 'joinedTheGroup'
| 'editGroupName'
| 'reportIssue'
| 'trimDatabase'
| 'trimDatabaseDescription'
| 'trimDatabaseConfirmationBody'
| 'reportIssue';

@ -2210,6 +2210,11 @@ bindings@^1.5.0:
dependencies:
file-uri-to-path "1.0.0"
bindings@~1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5"
integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==
biskviit@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/biskviit/-/biskviit-1.0.1.tgz#037a0cd4b71b9e331fd90a1122de17dc49e420a7"
@ -2590,7 +2595,7 @@ chai-bytes@^0.1.2:
resolved "https://registry.yarnpkg.com/chai-bytes/-/chai-bytes-0.1.2.tgz#c297e81d47eb3106af0676ded5bb5e0c9f981db3"
integrity sha512-0ol6oJS0y1ozj6AZK8n1pyv1/G+l44nqUJygAkK1UrYl+IOGie5vcrEdrAlwmLYGIA9NVvtHWosPYwWWIXf/XA==
chai@4.3.4:
chai@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49"
integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==
@ -2674,7 +2679,7 @@ chokidar@^2.1.8:
optionalDependencies:
fsevents "^1.2.7"
chownr@^1.1.1:
chownr@^1.1.1, chownr@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
@ -3454,6 +3459,11 @@ destroy@~1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
detect-libc@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
detect-node@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
@ -4565,7 +4575,7 @@ fs-extra@^9.0.1:
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-minipass@^1.2.5:
fs-minipass@^1.2.5, fs-minipass@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
@ -5295,7 +5305,7 @@ iconv-lite@0.4.13:
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
integrity sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=
iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -5316,6 +5326,13 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
dependencies:
postcss "^7.0.14"
ignore-walk@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335"
integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==
dependencies:
minimatch "^3.0.4"
ignore@^3.3.3:
version "3.3.10"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
@ -6166,6 +6183,15 @@ libsodium@0.7.8:
resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.8.tgz#fbd12247b7b1353f88d8de1cbc66bc1a07b2e008"
integrity sha512-/Qc+APf0jbeWSaeEruH0L1/tbbT+sbf884ZL0/zV/0JXaDPBzYkKbyb/wmxMHgAHzm3t6gqe7bOOXAVwfqVikQ==
libxmljs@^0.19.7:
version "0.19.7"
resolved "https://registry.yarnpkg.com/libxmljs/-/libxmljs-0.19.7.tgz#96c2151b0b73f33dd29917edec82902587004e5a"
integrity sha512-lFJyG9T1mVwTzNTw6ZkvIt0O+NsIR+FTE+RcC2QDFGU8YMnQrnyEOGrj6HWSe1AdwQK7s37BOp4NL+pcAqfK2g==
dependencies:
bindings "~1.3.0"
nan "~2.14.0"
node-pre-gyp "~0.11.0"
lie@*:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
@ -6701,7 +6727,7 @@ minipass@^3.0.0:
dependencies:
yallist "^4.0.0"
minizlib@^1.1.1:
minizlib@^1.1.1, minizlib@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
@ -6863,7 +6889,7 @@ mz@^2.3.1:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nan@2.14.2, nan@^2.13.2:
nan@2.14.2, nan@^2.13.2, nan@~2.14.0:
version "2.14.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
@ -6921,6 +6947,15 @@ ncp@~2.0.0:
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
needle@^2.2.1:
version "2.9.1"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.9.1.tgz#22d1dffbe3490c2b83e301f7709b6736cd8f2684"
integrity sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==
dependencies:
debug "^3.2.6"
iconv-lite "^0.4.4"
sax "^1.2.4"
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@ -6996,6 +7031,22 @@ node-modules-regexp@^1.0.0:
resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
node-pre-gyp@~0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054"
integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==
dependencies:
detect-libc "^1.0.2"
mkdirp "^0.5.1"
needle "^2.2.1"
nopt "^4.0.1"
npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.2.7"
rimraf "^2.6.1"
semver "^5.3.0"
tar "^4"
node-releases@^1.1.71:
version "1.1.73"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
@ -7041,6 +7092,14 @@ node-sass@6.0.1:
dependencies:
abbrev "1"
nopt@^4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
dependencies:
abbrev "1"
osenv "^0.1.4"
nopt@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
@ -7092,6 +7151,13 @@ normalize-url@^4.1.0:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
npm-bundled@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1"
integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==
dependencies:
npm-normalize-package-bin "^1.0.1"
npm-conf@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9"
@ -7100,6 +7166,20 @@ npm-conf@^1.1.3:
config-chain "^1.1.11"
pify "^3.0.0"
npm-normalize-package-bin@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
npm-packlist@^1.1.6:
version "1.4.8"
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
dependencies:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
npm-normalize-package-bin "^1.0.1"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@ -7114,7 +7194,7 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1:
dependencies:
path-key "^3.0.0"
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.1.2:
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2, npmlog@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@ -7282,7 +7362,7 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
osenv@0:
osenv@0, osenv@^0.1.4:
version "0.1.5"
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
@ -8027,7 +8107,7 @@ rc-util@^4.0.4, rc-util@^4.15.3, rc-util@^4.4.0:
react-lifecycles-compat "^3.0.4"
shallowequal "^1.1.0"
rc@^1.2.8:
rc@^1.2.7, rc@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
@ -8618,7 +8698,7 @@ retry@^0.12.0:
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
rimraf@2, rimraf@^2.2.8, rimraf@^2.6.3:
rimraf@2, rimraf@^2.2.8, rimraf@^2.6.1, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@ -8718,6 +8798,11 @@ safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2,
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
safe-buffer@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-json-stringify@~1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd"
@ -9597,6 +9682,19 @@ tar@^2.0.0:
fstream "^1.0.12"
inherits "2"
tar@^4:
version "4.4.19"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3"
integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==
dependencies:
chownr "^1.1.4"
fs-minipass "^1.2.7"
minipass "^2.9.0"
minizlib "^1.3.3"
mkdirp "^0.5.5"
safe-buffer "^5.2.1"
yallist "^3.1.1"
tar@^6.0.2, tar@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
@ -10496,7 +10594,7 @@ yallist@^2.1.2:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
yallist@^3.0.0, yallist@^3.0.2:
yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==

Loading…
Cancel
Save