diff --git a/Gruntfile.js b/Gruntfile.js
index 3e93ae5d1..365942e8f 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,6 +1,4 @@
const importOnce = require('node-sass-import-once');
-const rimraf = require('rimraf');
-const mkdirp = require('mkdirp');
const sass = require('node-sass');
/* eslint-disable more/no-then, no-console */
@@ -39,7 +37,6 @@ module.exports = grunt => {
'js/curve/curve25519_wrapper.js',
'node_modules/libsodium/dist/modules/libsodium.js',
'node_modules/libsodium-wrappers/dist/modules/libsodium-wrappers.js',
-
'libtextsecure/libsignal-protocol.js',
'js/util_worker_tasks.js',
];
@@ -169,11 +166,6 @@ module.exports = grunt => {
updateLocalConfig({ commitHash: hash });
});
- grunt.registerTask('clean-release', () => {
- rimraf.sync('release');
- mkdirp.sync('release');
- });
-
grunt.registerTask('dev', ['default', 'watch']);
grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
grunt.registerTask('default', [
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 50b27172b..182541cc9 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -156,6 +156,7 @@
"spellCheckDirty": "You must restart Session to apply your new settings",
"notifications": "Notifications",
"readReceiptSettingDescription": "See and share when messages have been read (enables read receipts in all sessions).",
+ "readReceiptDialogDescription": "Read Receipts are now turned ON by default. Click \"Cancel\" to turn them down.",
"readReceiptSettingTitle": "Read Receipts",
"typingIndicatorsSettingDescription": "See and share when messages are being typed (applies to all sessions).",
"typingIndicatorsSettingTitle": "Typing Indicators",
@@ -458,6 +459,7 @@
"noAudioOutputFound": "No audio output found",
"callMediaPermissionsTitle": "Voice and video calls",
"callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.",
+ "callMissedNotApproved": "Call missed from '$name$' as you haven't approved this conversation yet. Send a message to him first.",
"callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users",
"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",
diff --git a/app/logging.js b/app/logging.js
index 95a87d67e..0e5a7c025 100644
--- a/app/logging.js
+++ b/app/logging.js
@@ -6,7 +6,6 @@ const fs = require('fs');
const electron = require('electron');
const bunyan = require('bunyan');
-const mkdirp = require('mkdirp');
const _ = require('lodash');
const readFirstLine = require('firstline');
const readLastLines = require('read-last-lines').read;
@@ -31,7 +30,7 @@ function initialize() {
const basePath = app.getPath('userData');
const logPath = path.join(basePath, 'logs');
- mkdirp.sync(logPath);
+ fs.mkdirSync(logPath, { recursive: true });
return cleanupLogs(logPath).then(() => {
if (logger) {
@@ -63,7 +62,7 @@ function initialize() {
});
ipc.on('fetch-log', event => {
- mkdirp.sync(logPath);
+ fs.mkdirSync(logPath, { recursive: true });
fetch(logPath).then(
data => {
@@ -125,7 +124,7 @@ async function cleanupLogs(logPath) {
// delete and re-create the log directory
await deleteAllLogs(logPath);
- mkdirp.sync(logPath);
+ fs.mkdirSync(logPath, { recursive: true });
}
}
@@ -221,7 +220,7 @@ function fetch(logPath) {
// Check that the file exists locally
if (!fs.existsSync(logPath)) {
console._log('Log folder not found while fetching its content. Quick! Creating it.');
- mkdirp.sync(logPath);
+ fs.mkdirSync(logPath, { recursive: true });
}
const files = fs.readdirSync(logPath);
const paths = files.map(file => path.join(logPath, file));
diff --git a/app/profile_images.js b/app/profile_images.js
index 75b16a0c8..b2752215c 100644
--- a/app/profile_images.js
+++ b/app/profile_images.js
@@ -1,12 +1,11 @@
const fs = require('fs');
-const mkdirp = require('mkdirp');
const path = require('path');
const { app } = require('electron').remote;
const userDataPath = app.getPath('userData');
const PATH = path.join(userDataPath, 'profileImages');
-mkdirp.sync(PATH);
+fs.mkdirSync(PATH, { recursive: true });
const hasImage = pubKey => fs.existsSync(getImagePath(pubKey));
diff --git a/app/sql.js b/app/sql.js
index 0948d915b..d4b20e0db 100644
--- a/app/sql.js
+++ b/app/sql.js
@@ -1,5 +1,5 @@
const path = require('path');
-const mkdirp = require('mkdirp');
+const fs = require('fs');
const rimraf = require('rimraf');
const SQL = require('better-sqlite3');
const { app, dialog, clipboard } = require('electron');
@@ -72,6 +72,7 @@ module.exports = {
getNextExpiringMessage,
getMessagesByConversation,
getFirstUnreadMessageIdInConversation,
+ hasConversationOutgoingMessage,
getUnprocessedCount,
getAllUnprocessed,
@@ -1240,7 +1241,11 @@ function updateToLokiSchemaVersion17(currentVersion, db) {
UPDATE ${CONVERSATIONS_TABLE} SET
json = json_set(json, '$.isApproved', 1)
`);
-
+ // remove the moderators field. As it was only used for opengroups a long time ago and whatever is there is probably unused
+ db.exec(`
+ UPDATE ${CONVERSATIONS_TABLE} SET
+ json = json_remove(json, '$.moderators', '$.dataMessage', '$.accessKey', '$.profileSharing', '$.sessionRestoreSeen')
+ `);
writeLokiSchemaVersion(targetVersion, db);
})();
console.log(`updateToLokiSchemaVersion${targetVersion}: success!`);
@@ -1311,8 +1316,7 @@ let databaseFilePath;
function _initializePaths(configDir) {
const dbDir = path.join(configDir, 'sql');
- mkdirp.sync(dbDir);
-
+ fs.mkdirSync(dbDir, { recursive: true });
databaseFilePath = path.join(dbDir, 'db.sqlite');
}
@@ -1357,7 +1361,9 @@ function initialize({ configDir, key, messages, passwordAttempt }) {
// Clear any already deleted db entries on each app start.
vacuumDatabase(db);
const msgCount = getMessageCount();
- console.warn('total message count: ', msgCount);
+ const convoCount = getConversationCount();
+ console.info('total message count: ', msgCount);
+ console.info('total conversation count: ', convoCount);
} catch (error) {
if (passwordAttempt) {
throw error;
@@ -2171,6 +2177,27 @@ function getMessagesByConversation(
return map(rows, row => jsonToObject(row.json));
}
+function hasConversationOutgoingMessage(conversationId) {
+ const row = globalInstance
+ .prepare(
+ `
+ SELECT count(*) FROM ${MESSAGES_TABLE} WHERE
+ conversationId = $conversationId AND
+ type IS 'outgoing'
+ `
+ )
+ .get({
+ conversationId,
+ });
+ if (!row) {
+ throw new Error('hasConversationOutgoingMessage: Unable to get coun');
+ }
+
+ console.warn('hasConversationOutgoingMessage', row);
+
+ return Boolean(row['count(*)']);
+}
+
function getFirstUnreadMessageIdInConversation(conversationId) {
const rows = globalInstance
.prepare(
diff --git a/js/logging.js b/js/logging.js
index 96b1f632e..bb5a8020d 100644
--- a/js/logging.js
+++ b/js/logging.js
@@ -6,7 +6,6 @@
const { ipcRenderer } = require('electron');
const _ = require('lodash');
-const debuglogs = require('./modules/debuglogs');
const Privacy = require('./modules/privacy');
const ipc = ipcRenderer;
@@ -100,7 +99,6 @@ function fetch() {
});
}
-const publish = debuglogs.upload;
const development = window.getEnvironment() !== 'production';
// A modern logging interface for the browser
@@ -127,7 +125,6 @@ window.log = {
debug: _.partial(logAtLevel, 'debug', 'DEBUG'),
trace: _.partial(logAtLevel, 'trace', 'TRACE'),
fetch,
- publish,
};
window.onerror = (message, script, line, col, error) => {
diff --git a/js/modules/debuglogs.js b/js/modules/debuglogs.js
deleted file mode 100644
index 3fd76dd29..000000000
--- a/js/modules/debuglogs.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-env node */
-/* global window */
-
-const FormData = require('form-data');
-const insecureNodeFetch = require('node-fetch');
-
-const BASE_URL = 'https://debuglogs.org';
-const VERSION = window.getVersion();
-const USER_AGENT = `Session ${VERSION}`;
-
-// upload :: String -> Promise URL
-exports.upload = async content => {
- window.log.warn('insecureNodeFetch => upload debugLogs');
- const signedForm = await insecureNodeFetch(BASE_URL, {
- headers: {
- 'user-agent': USER_AGENT,
- },
- });
- const json = await signedForm.json();
- if (!signedForm.ok || !json) {
- throw new Error('Failed to retrieve token');
- }
- const { fields, url } = json;
-
- const form = new FormData();
- // The API expects `key` to be the first field:
- form.append('key', fields.key);
- Object.entries(fields)
- .filter(([key]) => key !== 'key')
- .forEach(([key, value]) => {
- form.append(key, value);
- });
-
- const contentBuffer = Buffer.from(content, 'utf8');
- const contentType = 'text/plain';
- form.append('Content-Type', contentType);
- form.append('file', contentBuffer, {
- contentType,
- filename: `session-desktop-debug-log-${VERSION}.txt`,
- });
-
- const result = await insecureNodeFetch(url, {
- method: 'POST',
- body: form,
- });
-
- const { status } = result;
- if (status !== 204) {
- throw new Error(`Failed to upload to S3, got status ${status}`);
- }
-
- return `${BASE_URL}/${fields.key}`;
-};
diff --git a/package.json b/package.json
index 5ae1ae014..1dceee41f 100644
--- a/package.json
+++ b/package.json
@@ -88,7 +88,6 @@
"lodash": "4.17.11",
"long": "^4.0.0",
"mic-recorder-to-mp3": "^2.2.2",
- "mkdirp": "0.5.1",
"moment": "2.21.0",
"mustache": "2.3.0",
"nan": "2.14.2",
@@ -153,7 +152,6 @@
"@types/libsodium-wrappers": "^0.7.8",
"@types/linkify-it": "2.0.3",
"@types/lodash": "4.14.106",
- "@types/mkdirp": "0.5.2",
"@types/mocha": "5.0.0",
"@types/node-fetch": "^2.5.7",
"@types/pify": "3.0.2",
diff --git a/stylesheets/_modal.scss b/stylesheets/_modal.scss
index 6d15be954..2387092a6 100644
--- a/stylesheets/_modal.scss
+++ b/stylesheets/_modal.scss
@@ -226,8 +226,8 @@
justify-content: center;
position: absolute;
right: -3px;
- height: 26px;
- width: 26px;
+ height: 30px;
+ width: 30px;
border-radius: 50%;
background-color: $session-color-white;
transition: $session-transition-duration;
diff --git a/stylesheets/_session_left_pane.scss b/stylesheets/_session_left_pane.scss
index ade13d4d4..a547920a9 100644
--- a/stylesheets/_session_left_pane.scss
+++ b/stylesheets/_session_left_pane.scss
@@ -149,12 +149,6 @@ $session-compose-margin: 20px;
&__list {
height: -webkit-fill-available;
-
- &-popup {
- width: -webkit-fill-available;
- height: -webkit-fill-available;
- position: absolute;
- }
}
&-overlay {
diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx
index 43b3270fe..9d748a52c 100644
--- a/ts/components/conversation/ConversationHeader.tsx
+++ b/ts/components/conversation/ConversationHeader.tsx
@@ -13,6 +13,7 @@ import {
getConversationHeaderProps,
getConversationHeaderTitleProps,
getCurrentNotificationSettingText,
+ getIsSelectedBlocked,
getIsSelectedNoteToSelf,
getIsSelectedPrivate,
getSelectedConversationIsPublic,
@@ -198,6 +199,7 @@ const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) =>
const CallButton = () => {
const isPrivate = useSelector(getIsSelectedPrivate);
+ const isBlocked = useSelector(getIsSelectedBlocked);
const isMe = useSelector(getIsSelectedNoteToSelf);
const selectedConvoKey = useSelector(getSelectedConversationKey);
@@ -205,7 +207,7 @@ const CallButton = () => {
const hasOngoingCall = useSelector(getHasOngoingCall);
const canCall = !(hasIncomingCall || hasOngoingCall);
- if (!isPrivate || isMe || !selectedConvoKey) {
+ if (!isPrivate || isMe || !selectedConvoKey || isBlocked) {
return null;
}
diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx
index 045e506cb..31038fb32 100644
--- a/ts/components/dialog/EditProfileDialog.tsx
+++ b/ts/components/dialog/EditProfileDialog.tsx
@@ -150,15 +150,14 @@ export class EditProfileDialog extends React.Component<{}, State> {
name="name"
onChange={this.onFileSelected}
/>
-
-
{
- this.setState(state => ({ ...state, mode: 'qr' }));
- }}
- />
+ {
+ this.setState(state => ({ ...state, mode: 'qr' }));
+ }}
+ role="button"
+ >
+
diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx
index 7ad2f323b..ef7800b1a 100644
--- a/ts/components/session/ActionsPanel.tsx
+++ b/ts/components/session/ActionsPanel.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { Dispatch, useEffect, useState } from 'react';
import { SessionIconButton } from './icon';
import { Avatar, AvatarSize } from '../Avatar';
import { SessionToastContainer } from './SessionToastContainer';
@@ -6,6 +6,7 @@ import { getConversationController } from '../../session/conversations';
import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils';
import {
+ createOrUpdateItem,
generateAttachmentKeyIfEmpty,
getAllOpenGroupV1Conversations,
getItemById,
@@ -36,7 +37,11 @@ import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool';
import { getSwarmPollingInstance } from '../../session/snode_api';
import { DURATION } from '../../session/constants';
import { conversationChanged, conversationRemoved } from '../../state/ducks/conversations';
-import { editProfileModal, onionPathModal } from '../../state/ducks/modalDialog';
+import {
+ editProfileModal,
+ onionPathModal,
+ updateConfirmModal,
+} from '../../state/ducks/modalDialog';
import { uploadOurAvatar } from '../../interactions/conversationInteractions';
import { ModalContainer } from '../dialog/ModalContainer';
import { debounce } from 'underscore';
@@ -49,7 +54,29 @@ import { switchHtmlToDarkTheme, switchHtmlToLightTheme } from '../../state/ducks
import { DraggableCallContainer } from './calling/DraggableCallContainer';
import { IncomingCallDialog } from './calling/IncomingCallDialog';
import { CallInFullScreenContainer } from './calling/CallInFullScreenContainer';
-
+import { SessionButtonColor } from './SessionButton';
+import { settingsReadReceipt } from './settings/section/CategoryPrivacy';
+
+async function showTurnOnReadAck(dispatch: Dispatch) {
+ const singleShotSettingId = 'read-receipt-turn-on-asked';
+ const item = (await getItemById(singleShotSettingId))?.value || false;
+
+ if (!item) {
+ await createOrUpdateItem({ id: singleShotSettingId, value: true });
+ // set it to true by default, user will be asked to willingfully turn it off
+ window.setSettingValue(settingsReadReceipt, true);
+ dispatch(
+ updateConfirmModal({
+ title: window.i18n('readReceiptSettingTitle'),
+ messageSub: window.i18n('readReceiptDialogDescription'),
+ okTheme: SessionButtonColor.Green,
+ onClickCancel: () => {
+ window.setSettingValue(settingsReadReceipt, false);
+ },
+ })
+ );
+ }
+}
const Section = (props: { type: SectionType }) => {
const ourNumber = useSelector(getOurNumber);
const unreadMessageCount = useSelector(getUnreadMessageCount);
@@ -227,7 +254,7 @@ const triggerAvatarReUploadIfNeeded = async () => {
/**
* This function is called only once: on app startup with a logged in user
*/
-const doAppStartUp = () => {
+const doAppStartUp = (dispatch: Dispatch) => {
// init the messageQueue. In the constructor, we add all not send messages
// this call does nothing except calling the constructor, which will continue sending message in the pipeline
void getMessageQueue().processAllPending();
@@ -246,6 +273,8 @@ const doAppStartUp = () => {
void loadDefaultRooms();
+ void showTurnOnReadAck(dispatch);
+
debounce(triggerAvatarReUploadIfNeeded, 200);
};
@@ -267,10 +296,11 @@ export const ActionsPanel = () => {
const [startCleanUpMedia, setStartCleanUpMedia] = useState(false);
const ourPrimaryConversation = useSelector(getOurPrimaryConversation);
+ const dispatch = useDispatch();
// this maxi useEffect is called only once: when the component is mounted.
// For the action panel, it means this is called only one per app start/with a user loggedin
useEffect(() => {
- void doAppStartUp();
+ void doAppStartUp(dispatch);
}, []);
// wait for cleanUpMediasInterval and then start cleaning up medias
diff --git a/ts/components/session/LeftPaneContactSection.tsx b/ts/components/session/LeftPaneContactSection.tsx
index e1f2693dc..e1f8a931c 100644
--- a/ts/components/session/LeftPaneContactSection.tsx
+++ b/ts/components/session/LeftPaneContactSection.tsx
@@ -53,7 +53,7 @@ const ContactListItemSection = () => {
export const LeftPaneContactSection = () => {
return (
-
+
diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx
index 5407b0490..4d0342b56 100644
--- a/ts/components/session/LeftPaneMessageSection.tsx
+++ b/ts/components/session/LeftPaneMessageSection.tsx
@@ -139,22 +139,12 @@ export class LeftPaneMessageSection extends React.Component
{
}
}
- public renderHeader(): JSX.Element {
- return (
-
- );
- }
-
public render(): JSX.Element {
const { overlay } = this.state;
return (
- {this.renderHeader()}
+
{overlay ? this.renderClosableOverlay() : this.renderConversations()}
);
diff --git a/ts/components/session/LeftPaneSectionHeader.tsx b/ts/components/session/LeftPaneSectionHeader.tsx
index 3e734e827..07b5a8df8 100644
--- a/ts/components/session/LeftPaneSectionHeader.tsx
+++ b/ts/components/session/LeftPaneSectionHeader.tsx
@@ -1,6 +1,4 @@
import React from 'react';
-import classNames from 'classnames';
-import { SessionIcon, SessionIconType } from './icon';
import styled from 'styled-components';
import { SessionButton, SessionButtonType } from './SessionButton';
import { useDispatch, useSelector } from 'react-redux';
@@ -11,52 +9,42 @@ import { Flex } from '../basic/Flex';
import { getFocusedSection } from '../../state/selectors/section';
import { SectionType } from '../../state/ducks/section';
import { UserUtils } from '../../session/utils';
+import { SessionIcon } from './icon';
-const Tab = ({
- isSelected,
- label,
- onSelect,
- type,
-}: {
- isSelected: boolean;
- label: string;
- onSelect?: (event: number) => void;
- type: number;
-}) => {
- const handleClick = onSelect
- ? () => {
- onSelect(type);
- }
- : undefined;
-
- return (
-
- {label}
-
- );
-};
-
-type Props = {
- label?: string;
- buttonIcon?: SessionIconType;
- buttonClicked?: any;
-};
+const SectionTitle = styled.h1`
+ padding: 0 var(--margins-sm);
+ flex-grow: 1;
+ color: var(--color-text);
+`;
-export const LeftPaneSectionHeader = (props: Props) => {
- const { label, buttonIcon, buttonClicked } = props;
+export const LeftPaneSectionHeader = (props: { buttonClicked?: any }) => {
const showRecoveryPhrasePrompt = useSelector(getShowRecoveryPhrasePrompt);
+ const focusedSection = useSelector(getFocusedSection);
+
+ let label: string | undefined;
+
+ const isMessageSection = focusedSection === SectionType.Message;
+
+ switch (focusedSection) {
+ case SectionType.Contact:
+ label = window.i18n('contactsHeader');
+ break;
+ case SectionType.Settings:
+ label = window.i18n('settingsHeader');
+ break;
+ case SectionType.Message:
+ label = window.i18n('messagesHeader');
+ break;
+ default:
+ }
return (
- {label && }
- {buttonIcon && (
-
-
+ {label}
+ {isMessageSection && (
+
+
)}
diff --git a/ts/components/session/LeftPaneSettingSection.tsx b/ts/components/session/LeftPaneSettingSection.tsx
index 73ba038bf..1f5ff2181 100644
--- a/ts/components/session/LeftPaneSettingSection.tsx
+++ b/ts/components/session/LeftPaneSettingSection.tsx
@@ -113,7 +113,7 @@ const LeftPaneBottomButtons = () => {
export const LeftPaneSettingSection = () => {
return (
-
+
diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx
index e19642439..3869da384 100644
--- a/ts/components/session/MessageRequestsBanner.tsx
+++ b/ts/components/session/MessageRequestsBanner.tsx
@@ -26,9 +26,9 @@ const StyledMessageRequestBannerHeader = styled.span`
font-weight: bold;
font-size: 15px;
color: var(--color-text-subtle);
- padding-left: var(--margin-xs);
+ padding-left: var(--margins-xs);
margin-inline-start: 12px;
- margin-top: var(--margin-sm);
+ margin-top: var(--margins-sm);
line-height: 18px;
overflow-x: hidden;
overflow-y: hidden;
@@ -37,7 +37,7 @@ const StyledMessageRequestBannerHeader = styled.span`
`;
const StyledCircleIcon = styled.div`
- padding-left: var(--margin-xs);
+ padding-left: var(--margins-xs);
`;
const StyledUnreadCounter = styled.div`
diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx
index 897be4bb7..a7ee1b402 100644
--- a/ts/components/session/SessionClosableOverlay.tsx
+++ b/ts/components/session/SessionClosableOverlay.tsx
@@ -13,6 +13,7 @@ import { useSelector } from 'react-redux';
import { getConversationRequests } from '../../state/selectors/conversations';
import { MemoConversationListItemWithDetails } from '../ConversationListItem';
import styled from 'styled-components';
+// tslint:disable: use-simple-attributes
export enum SessionClosableOverlayType {
Message = 'message',
@@ -178,7 +179,7 @@ export class SessionClosableOverlay extends React.Component
{
placeholder={placeholder}
value={groupName}
isGroup={true}
- maxLength={100}
+ maxLength={isOpenGroupView ? 300 : 100}
onChange={this.onGroupNameChanged}
onPressEnter={() => onButtonClick(groupName, selectedMembers)}
/>
diff --git a/ts/components/session/SessionToastContainer.tsx b/ts/components/session/SessionToastContainer.tsx
index 7788a1e0b..8beeb647d 100644
--- a/ts/components/session/SessionToastContainer.tsx
+++ b/ts/components/session/SessionToastContainer.tsx
@@ -6,7 +6,7 @@ const SessionToastContainerPrivate = () => {
return (
{
return (