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 ( - {getStartCallMenuItem(conversationId)} {getDisappearingMenuItem(isPublic, isKickedFromGroup, left, isBlocked, conversationId)} {getNotificationForConvoMenuItem({ isKickedFromGroup, diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 9b05d96d5..08ac8643c 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { getHasIncomingCall, getHasOngoingCall } from '../../../state/selectors/call'; import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations'; import { getFocusedSection } from '../../../state/selectors/section'; import { Item, Submenu } from 'react-contexify'; @@ -18,7 +17,6 @@ import { SectionType } from '../../../state/ducks/section'; import { getConversationController } from '../../../session/conversations'; import { blockConvoById, - callRecipient, clearNickNameByConvoId, copyPublicKeyByConvoId, deleteAllMessagesByConvoIdWithConfirmation, @@ -347,32 +345,6 @@ export function getMarkAllReadMenuItem(conversationId: string): JSX.Element | nu ); } -export function getStartCallMenuItem(conversationId: string): JSX.Element | null { - if (window?.lokiFeatureFlags.useCallMessage) { - const convoOut = getConversationController().get(conversationId); - // we don't support calling groups - - const hasIncomingCall = useSelector(getHasIncomingCall); - const hasOngoingCall = useSelector(getHasOngoingCall); - const canCall = !(hasIncomingCall || hasOngoingCall); - if (!convoOut?.isPrivate() || convoOut.isMe()) { - return null; - } - - return ( - { - void callRecipient(conversationId, canCall); - }} - > - {window.i18n('menuCall')} - - ); - } - - return null; -} - export function getDisappearingMenuItem( isPublic: boolean | undefined, isKickedFromGroup: boolean | undefined, diff --git a/ts/components/session/settings/section/CategoryPrivacy.tsx b/ts/components/session/settings/section/CategoryPrivacy.tsx index 0ed432602..c6eea2df3 100644 --- a/ts/components/session/settings/section/CategoryPrivacy.tsx +++ b/ts/components/session/settings/section/CategoryPrivacy.tsx @@ -10,7 +10,7 @@ import { PasswordAction } from '../../../dialog/SessionPasswordDialog'; import { SessionButtonColor } from '../../SessionButton'; import { SessionSettingButtonItem, SessionToggleWithDescription } from '../SessionSettingListItem'; -const settingsReadReceipt = 'read-receipt-setting'; +export const settingsReadReceipt = 'read-receipt-setting'; const settingsTypingIndicator = 'typing-indicators-setting'; const settingsAutoUpdate = 'auto-update'; diff --git a/ts/data/data.ts b/ts/data/data.ts index 6924c6355..e5cc48e59 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -120,6 +120,7 @@ const channelsToMake = { getNextExpiringMessage, getMessagesByConversation, getFirstUnreadMessageIdInConversation, + hasConversationOutgoingMessage, getSeenMessagesByHashList, getLastHashBySnode, @@ -763,6 +764,9 @@ export async function getFirstUnreadMessageIdInConversation( return channels.getFirstUnreadMessageIdInConversation(conversationId); } +export async function hasConversationOutgoingMessage(conversationId: string): Promise { + return channels.hasConversationOutgoingMessage(conversationId); +} export async function getLastHashBySnode(convoId: string, snode: string): Promise { return channels.getLastHashBySnode(convoId, snode); } diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index b2209829f..12bec5b3a 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -82,12 +82,10 @@ export interface ConversationAttributes { active_at: number; lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group groupAdmins?: Array; - moderators?: Array; // TODO to merge to groupAdmins with a migration on the db isKickedFromGroup?: boolean; avatarPath?: string; isMe?: boolean; subscriberCount?: number; - sessionRestoreSeen?: boolean; is_medium_group?: boolean; type: string; avatarPointer?: string; @@ -124,12 +122,10 @@ export interface ConversationAttributesOptionals { timestamp?: number; // timestamp of what? lastJoinedTimestamp?: number; groupAdmins?: Array; - moderators?: Array; isKickedFromGroup?: boolean; avatarPath?: string; isMe?: boolean; subscriberCount?: number; - sessionRestoreSeen?: boolean; is_medium_group?: boolean; type: string; avatarPointer?: string; @@ -164,11 +160,9 @@ export const fillConvoAttributesWithDefaults = ( lastMessageStatus: null, lastJoinedTimestamp: new Date('1970-01-01Z00:00:00:000').getTime(), groupAdmins: [], - moderators: [], isKickedFromGroup: false, isMe: false, subscriberCount: 0, - sessionRestoreSeen: false, is_medium_group: false, lastMessage: null, expireTimer: 0, @@ -280,6 +274,7 @@ export class ConversationModel extends Backbone.Model { public isMediumGroup() { return this.get('is_medium_group'); } + /** * Returns true if this conversation is active * i.e. the conversation is visibie on the left pane. (Either we or another user created this convo). @@ -290,99 +285,6 @@ export class ConversationModel extends Backbone.Model { return Boolean(this.get('active_at')); } - public async bumpTyping() { - // We don't send typing messages if the setting is disabled - // or we blocked that user - if ( - this.isPublic() || - this.isMediumGroup() || - !this.isActive() || - !window.storage.get('typing-indicators-setting') || - this.isBlocked() - ) { - return; - } - - if (!this.typingRefreshTimer) { - const isTyping = true; - this.setTypingRefreshTimer(); - this.sendTypingMessage(isTyping); - } - - this.setTypingPauseTimer(); - } - - public setTypingRefreshTimer() { - if (this.typingRefreshTimer) { - global.clearTimeout(this.typingRefreshTimer); - } - this.typingRefreshTimer = global.setTimeout(this.onTypingRefreshTimeout.bind(this), 10 * 1000); - } - - public onTypingRefreshTimeout() { - const isTyping = true; - this.sendTypingMessage(isTyping); - - // This timer will continue to reset itself until the pause timer stops it - this.setTypingRefreshTimer(); - } - - public setTypingPauseTimer() { - if (this.typingPauseTimer) { - global.clearTimeout(this.typingPauseTimer); - } - this.typingPauseTimer = global.setTimeout(this.onTypingPauseTimeout.bind(this), 10 * 1000); - } - - public onTypingPauseTimeout() { - const isTyping = false; - this.sendTypingMessage(isTyping); - - this.clearTypingTimers(); - } - - public clearTypingTimers() { - if (this.typingPauseTimer) { - global.clearTimeout(this.typingPauseTimer); - this.typingPauseTimer = null; - } - if (this.typingRefreshTimer) { - global.clearTimeout(this.typingRefreshTimer); - this.typingRefreshTimer = null; - } - } - - public sendTypingMessage(isTyping: boolean) { - if (!this.isPrivate()) { - return; - } - - const recipientId = this.id; - - if (!recipientId) { - throw new Error('Need to provide either recipientId'); - } - - const primaryDevicePubkey = window.storage.get('primaryDevicePubKey'); - if (recipientId && primaryDevicePubkey === recipientId) { - // note to self - return; - } - - const typingParams = { - timestamp: Date.now(), - isTyping, - typingTimestamp: Date.now(), - }; - const typingMessage = new TypingMessage(typingParams); - - // send the message to a single recipient if this is a session chat - const device = new PubKey(recipientId); - getMessageQueue() - .sendToPubKey(device, typingMessage) - .catch(window?.log?.error); - } - public async cleanup() { const { deleteAttachmentData } = window.Signal.Migrations; await window.Signal.Types.Conversation.deleteExternalFiles(this.attributes, { @@ -409,12 +311,10 @@ export class ConversationModel extends Backbone.Model { // removeMessage(); } - public getGroupAdmins() { + public getGroupAdmins(): Array { const groupAdmins = this.get('groupAdmins'); - if (groupAdmins?.length) { - return groupAdmins; - } - return this.get('moderators'); + + return groupAdmins && groupAdmins?.length > 0 ? groupAdmins : []; } // tslint:disable-next-line: cyclomatic-complexity @@ -558,9 +458,6 @@ export class ConversationModel extends Backbone.Model { const newAdmins = _.uniq(_.sortBy(groupAdmins)); if (_.isEqual(existingAdmins, newAdmins)) { - // window?.log?.info( - // 'Skipping updates of groupAdmins/moderators. No change detected.' - // ); return; } this.set({ groupAdmins }); @@ -694,7 +591,8 @@ export class ConversationModel extends Backbone.Model { return { author: quotedMessage.getSource(), id: `${quotedMessage.get('sent_at')}` || '', - text: body, + // no need to quote the full message length. + text: body?.slice(0, 100), attachments: quotedAttachments, timestamp: quotedMessage.get('sent_at') || 0, convoId: this.id, @@ -1626,7 +1524,6 @@ export class ConversationModel extends Backbone.Model { await this.commit(); } } else { - // tslint:disable-next-line: no-dynamic-delete this.typingTimer = null; if (wasTyping) { // User was previously typing, and is no longer. State change! @@ -1635,7 +1532,7 @@ export class ConversationModel extends Backbone.Model { } } - public async clearContactTypingTimer(_sender: string) { + private async clearContactTypingTimer(_sender: string) { if (!!this.typingTimer) { global.clearTimeout(this.typingTimer); this.typingTimer = null; @@ -1654,6 +1551,112 @@ export class ConversationModel extends Backbone.Model { return typeof expireTimer === 'number' && expireTimer > 0; } + + private shouldDoTyping() { + // for typing to happen, this must be a private unblocked active convo, and the settings to be on + if ( + !this.isActive() || + !window.storage.get('typing-indicators-setting') || + this.isBlocked() || + !this.isPrivate() + ) { + return false; + } + const msgRequestsEnabled = + window.lokiFeatureFlags.useMessageRequests && + window.inboxStore?.getState().userConfig.messageRequests; + + // if msg requests are unused, we have to send typing (this is already a private active unblocked convo) + if (!msgRequestsEnabled) { + return true; + } + // with message requests in use, we just need to check for isApproved + return Boolean(this.get('isApproved')); + } + + private async bumpTyping() { + if (!this.shouldDoTyping()) { + return; + } + + if (!this.typingRefreshTimer) { + const isTyping = true; + this.setTypingRefreshTimer(); + this.sendTypingMessage(isTyping); + } + + this.setTypingPauseTimer(); + } + + private setTypingRefreshTimer() { + if (this.typingRefreshTimer) { + global.clearTimeout(this.typingRefreshTimer); + } + this.typingRefreshTimer = global.setTimeout(this.onTypingRefreshTimeout.bind(this), 10 * 1000); + } + + private onTypingRefreshTimeout() { + const isTyping = true; + this.sendTypingMessage(isTyping); + + // This timer will continue to reset itself until the pause timer stops it + this.setTypingRefreshTimer(); + } + + private setTypingPauseTimer() { + if (this.typingPauseTimer) { + global.clearTimeout(this.typingPauseTimer); + } + this.typingPauseTimer = global.setTimeout(this.onTypingPauseTimeout.bind(this), 10 * 1000); + } + + private onTypingPauseTimeout() { + const isTyping = false; + this.sendTypingMessage(isTyping); + + this.clearTypingTimers(); + } + + private clearTypingTimers() { + if (this.typingPauseTimer) { + global.clearTimeout(this.typingPauseTimer); + this.typingPauseTimer = null; + } + if (this.typingRefreshTimer) { + global.clearTimeout(this.typingRefreshTimer); + this.typingRefreshTimer = null; + } + } + + private sendTypingMessage(isTyping: boolean) { + if (!this.isPrivate()) { + return; + } + + const recipientId = this.id; + + if (!recipientId) { + throw new Error('Need to provide either recipientId'); + } + + if (this.isMe()) { + // note to self + return; + } + + const typingParams = { + timestamp: Date.now(), + isTyping, + typingTimestamp: Date.now(), + }; + const typingMessage = new TypingMessage(typingParams); + + // send the message to a single recipient if this is a session chat + const device = new PubKey(recipientId); + getMessageQueue() + .sendToPubKey(device, typingMessage) + .catch(window?.log?.error); + } } export class ConversationCollection extends Backbone.Collection { diff --git a/ts/models/message.ts b/ts/models/message.ts index f7f75bdee..d41e9a5c4 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -1117,18 +1117,6 @@ export class MessageModel extends Backbone.Model { await this.commit(); } - public async markMessageSyncOnly(dataMessage: DataMessage) { - this.set({ - // These are the same as a normal send() - dataMessage, - sent_to: [UserUtils.getOurPubKeyStrFromCache()], - sent: true, - expirationStartTimestamp: Date.now(), - }); - - await this.commit(); - } - public async saveErrors(providedErrors: any) { let errors = providedErrors; diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index 643b511d2..0730f51ea 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -49,7 +49,7 @@ export interface MessageAttributes { */ timestamp?: number; status?: MessageDeliveryStatus; - dataMessage: any; + // dataMessage: any; sent_to: any; sent: boolean; diff --git a/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts b/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts index 5c7fb0738..cfa4278f6 100644 --- a/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts @@ -199,10 +199,12 @@ export class OpenGroupManagerV2 { await saveV2OpenGroupRoom(room); // mark active so it's not in the contacts list but in the conversation list + // mark isApproved as this is a public chat conversation.set({ active_at: Date.now(), name: room.roomName, avatarPath: room.roomName, + isApproved: true, }); await conversation.commit(); diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index e396d76a1..ae2d563df 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -123,7 +123,7 @@ const handleContactReceived = async ( envelope: EnvelopePlus ) => { try { - if (!contactReceived.publicKey) { + if (!contactReceived.publicKey?.length) { return; } const contactConvo = await getConversationController().getOrCreateAndWait( @@ -134,8 +134,11 @@ const handleContactReceived = async ( displayName: contactReceived.name, profilePictre: contactReceived.profilePicture, }; - // updateProfile will do a commit for us - contactConvo.set('active_at', _.toNumber(envelope.timestamp)); + + const existingActiveAt = contactConvo.get('active_at'); + if (!existingActiveAt || existingActiveAt === 0) { + contactConvo.set('active_at', _.toNumber(envelope.timestamp)); + } if ( window.lokiFeatureFlags.useMessageRequests && diff --git a/ts/session/utils/RingingManager.ts b/ts/session/utils/RingingManager.ts index 5357afb3d..9bd62b6a8 100644 --- a/ts/session/utils/RingingManager.ts +++ b/ts/session/utils/RingingManager.ts @@ -17,7 +17,7 @@ function startRinging() { ringingAudio.loop = true; ringingAudio.volume = 0.6; } - void ringingAudio.play(); + void ringingAudio.play().catch(window.log.info); } export function getIsRinging() { diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 36541e582..722005e12 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -24,7 +24,8 @@ export function pushToastInfo( id: string, title: string, description?: string, - onToastClick?: () => void + onToastClick?: () => void, + delay?: number ) { toast.info( , - { toastId: id, updateId: id } + { toastId: id, updateId: id, delay } ); } @@ -166,6 +167,14 @@ export function pushedMissedCallCauseOfPermission(conversationName: string) { ); } +export function pushedMissedCallNotApproved(displayName: string) { + pushToastInfo( + 'missedCall', + window.i18n('callMissedTitle'), + window.i18n('callMissedNotApproved', [displayName]) + ); +} + export function pushVideoCallPermissionNeeded() { pushToastInfo( 'videoCallPermissionNeeded', diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts index b318a0bf0..80b1a5430 100644 --- a/ts/session/utils/calling/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -20,11 +20,12 @@ import { PubKey } from '../../types'; import { v4 as uuidv4 } from 'uuid'; import { PnServer } from '../../../pushnotification'; -import { getIsRinging, setIsRinging } from '../RingingManager'; +import { getIsRinging } from '../RingingManager'; import { getBlackSilenceMediaStream } from './Silence'; import { getMessageQueue } from '../..'; import { MessageSender } from '../../sending'; import { DURATION } from '../../constants'; +import { hasConversationOutgoingMessage } from '../../../data/data'; // tslint:disable: function-name @@ -386,10 +387,20 @@ async function createOfferAndSendIt(recipient: string) { } if (offer && offer.sdp) { + const lines = offer.sdp.split(/\r?\n/); + const lineWithFtmpIndex = lines.findIndex(f => f.startsWith('a=fmtp:111')); + const partBeforeComma = lines[lineWithFtmpIndex].split(';'); + lines[lineWithFtmpIndex] = `${partBeforeComma[0]};cbr=1`; + let overridenSdps = lines.join('\n'); + overridenSdps = overridenSdps.replace( + new RegExp('.+urn:ietf:params:rtp-hdrext:ssrc-audio-level.*\\r?\\n'), + '' + ); + const offerMessage = new CallMessage({ timestamp: Date.now(), type: SignalService.CallMessage.Type.OFFER, - sdps: [offer.sdp], + sdps: [overridenSdps], uuid: currentCallUUID, }); @@ -497,7 +508,6 @@ export async function USER_callRecipient(recipient: string) { void PnServer.notifyPnServer(wrappedEnvelope, recipient); await openMediaDevicesAndAddTracks(); - setIsRinging(true); await createOfferAndSendIt(recipient); // close and end the call if callTimeoutMs is reached ans still not connected @@ -583,7 +593,6 @@ function handleConnectionStateChanged(pubkey: string) { if (peerConnection?.signalingState === 'closed' || peerConnection?.connectionState === 'failed') { closeVideoCall(); } else if (peerConnection?.connectionState === 'connected') { - setIsRinging(false); const firstAudioInput = audioInputsList?.[0].deviceId || undefined; if (firstAudioInput) { void selectAudioInputByDeviceId(firstAudioInput); @@ -603,7 +612,6 @@ function handleConnectionStateChanged(pubkey: string) { function closeVideoCall() { window.log.info('closingVideoCall '); currentCallStartTimestamp = undefined; - setIsRinging(false); if (peerConnection) { peerConnection.ontrack = null; peerConnection.onicecandidate = null; @@ -687,7 +695,6 @@ function onDataChannelReceivedMessage(ev: MessageEvent) { } function onDataChannelOnOpen() { window.log.info('onDataChannelOnOpen: sending video status'); - setIsRinging(false); sendVideoStatusViaDataChannel(); } @@ -747,7 +754,6 @@ function createOrGetPeerConnection(withPubkey: string) { export async function USER_acceptIncomingCallRequest(fromSender: string) { window.log.info('USER_acceptIncomingCallRequest'); - setIsRinging(false); if (currentCallUUID) { window.log.warn( 'Looks like we are already in a call as in USER_acceptIncomingCallRequest is not undefined' @@ -828,7 +834,6 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { } export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUID: string) { - setIsRinging(false); window.log.info(`rejectCallAlreadyAnotherCall ${ed25519Str(fromSender)}: ${forcedUUID}`); rejectedCallUUIDS.add(forcedUUID); const rejectCallMessage = new CallMessage({ @@ -843,7 +848,6 @@ export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUI } export async function USER_rejectIncomingCallRequest(fromSender: string) { - setIsRinging(false); // close the popup call window.inboxStore?.dispatch(endCall()); const lastOfferMessage = findLastMessageTypeFromSender( @@ -943,8 +947,6 @@ export async function handleCallTypeEndCall(sender: string, aboutCallUUID?: stri (ongoingCallStatus === 'incoming' || ongoingCallStatus === 'connecting') ) { // remote user hangup an offer he sent but we did not accept it yet - setIsRinging(false); - window.inboxStore?.dispatch(endCall()); } } @@ -993,6 +995,18 @@ function getCachedMessageFromCallMessage( }; } +async function isUserApprovedOrWeSentAMessage(user: string) { + const isApproved = getConversationController() + .get(user) + ?.isApproved(); + + if (isApproved) { + return true; + } + + return hasConversationOutgoingMessage(user); +} + export async function handleCallTypeOffer( sender: string, callMessage: SignalService.CallMessage, @@ -1009,7 +1023,16 @@ export async function handleCallTypeOffer( const cachedMsg = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp); pushCallMessageToCallCache(sender, remoteCallUUID, cachedMsg); - await handleMissedCall(sender, incomingOfferTimestamp, true); + await handleMissedCall(sender, incomingOfferTimestamp, 'permissions'); + return; + } + + const shouldDisplayOffer = await isUserApprovedOrWeSentAMessage(sender); + if (!shouldDisplayOffer) { + const cachedMsg = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp); + pushCallMessageToCallCache(sender, remoteCallUUID, cachedMsg); + + await handleMissedCall(sender, incomingOfferTimestamp, 'not-approved'); return; } @@ -1022,7 +1045,7 @@ export async function handleCallTypeOffer( return; } // add a message in the convo with this user about the missed call. - await handleMissedCall(sender, incomingOfferTimestamp, false); + await handleMissedCall(sender, incomingOfferTimestamp, 'another-call-ongoing'); // Here, we are in a call, and we got an offer from someone we are in a call with, and not one of his other devices. // Just hangup automatically the call on the calling side. @@ -1066,7 +1089,6 @@ export async function handleCallTypeOffer( } else if (callerConvo) { await callerConvo.notifyIncomingCall(); } - setIsRinging(true); } const cachedMessage = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp); @@ -1079,22 +1101,26 @@ export async function handleCallTypeOffer( export async function handleMissedCall( sender: string, incomingOfferTimestamp: number, - isBecauseOfCallPermission: boolean + reason: 'not-approved' | 'permissions' | 'another-call-ongoing' ) { const incomingCallConversation = getConversationController().get(sender); - setIsRinging(false); - if (!isBecauseOfCallPermission) { - ToastUtils.pushedMissedCall( - incomingCallConversation?.getNickname() || - incomingCallConversation?.getProfileName() || - 'Unknown' - ); - } else { - ToastUtils.pushedMissedCallCauseOfPermission( - incomingCallConversation?.getNickname() || - incomingCallConversation?.getProfileName() || - 'Unknown' - ); + + const displayname = + incomingCallConversation?.getNickname() || + incomingCallConversation?.getProfileName() || + 'Unknown'; + + switch (reason) { + case 'permissions': + ToastUtils.pushedMissedCallCauseOfPermission(displayname); + break; + case 'another-call-ongoing': + ToastUtils.pushedMissedCall(displayname); + break; + case 'not-approved': + ToastUtils.pushedMissedCallNotApproved(displayname); + break; + default: } await addMissedCallMessage(sender, incomingOfferTimestamp); diff --git a/ts/state/ducks/call.tsx b/ts/state/ducks/call.tsx index 7f0c55a27..140469640 100644 --- a/ts/state/ducks/call.tsx +++ b/ts/state/ducks/call.tsx @@ -1,4 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { setIsRinging } from '../../session/utils/RingingManager'; export type CallStatusEnum = 'offering' | 'incoming' | 'connecting' | 'ongoing' | undefined; @@ -31,11 +32,13 @@ const callSlice = createSlice({ } state.ongoingWith = callerPubkey; state.ongoingCallStatus = 'incoming'; + setIsRinging(true); return state; }, endCall(state: CallStateType) { state.ongoingCallStatus = undefined; state.ongoingWith = undefined; + setIsRinging(false); return state; }, @@ -50,6 +53,7 @@ const callSlice = createSlice({ } state.ongoingCallStatus = 'connecting'; state.callIsInFullScreen = false; + setIsRinging(false); return state; }, callConnected(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { @@ -66,6 +70,7 @@ const callSlice = createSlice({ ); return state; } + setIsRinging(false); state.ongoingCallStatus = 'ongoing'; state.callIsInFullScreen = false; @@ -80,6 +85,7 @@ const callSlice = createSlice({ window.log.warn('cannot start a call with an ongoing call already: ongoingCallStatus'); return state; } + setIsRinging(true); const callerPubkey = action.payload.pubkey; state.ongoingWith = callerPubkey; diff --git a/ts/state/ducks/section.tsx b/ts/state/ducks/section.tsx index e5332d8f8..43153a970 100644 --- a/ts/state/ducks/section.tsx +++ b/ts/state/ducks/section.tsx @@ -7,7 +7,6 @@ export enum SectionType { Profile, Message, Contact, - Channel, Settings, Moon, PathIndicator, diff --git a/ts/state/ducks/userConfig.tsx b/ts/state/ducks/userConfig.tsx index 57af32fdc..8f6fcd9f3 100644 --- a/ts/state/ducks/userConfig.tsx +++ b/ts/state/ducks/userConfig.tsx @@ -13,7 +13,7 @@ export interface UserConfigState { export const initialUserConfigState = { audioAutoplay: false, showRecoveryPhrasePrompt: true, - messageRequests: true, + messageRequests: false, }; const userConfigSlice = createSlice({ diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 9d15cc59d..dec166c96 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -539,6 +539,13 @@ export const getIsSelectedPrivate = createSelector( } ); +export const getIsSelectedBlocked = createSelector( + getConversationHeaderProps, + (headerProps): boolean => { + return headerProps?.isBlocked || false; + } +); + export const getIsSelectedNoteToSelf = createSelector( getConversationHeaderProps, (headerProps): boolean => { diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 75ff81dd3..148244bbd 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -94,6 +94,7 @@ export type LocalizerKeys = | 'pinConversation' | 'lightboxImageAlt' | 'linkDevice' + | 'callMissedNotApproved' | 'goToOurSurvey' | 'invalidPubkeyFormat' | 'disappearingMessagesDisabled' @@ -208,6 +209,7 @@ export type LocalizerKeys = | 'timerOption_0_seconds_abbreviated' | 'timerOption_5_minutes_abbreviated' | 'enterOptionalPassword' + | 'userRemovedFromModerators' | 'goToReleaseNotes' | 'unpinConversation' | 'viewMenuResetZoom' @@ -339,7 +341,7 @@ export type LocalizerKeys = | 'youDisabledDisappearingMessages' | 'updateGroupDialogTitle' | 'surveyTitle' - | 'userRemovedFromModerators' + | 'readReceiptDialogDescription' | 'timerOption_5_seconds' | 'failedToRemoveFromModerator' | 'conversationsHeader' diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 74cde12f0..218a4ac2a 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -148,10 +148,13 @@ async function createAccount(identityKeyPair: any) { await window.textsecure.storage.put('identityKey', identityKeyPair); await window.textsecure.storage.put('password', password); - await window.textsecure.storage.put('read-receipt-setting', false); + + // enable read-receipt by default + await window.textsecure.storage.put('read-receipt-setting', true); + await window.textsecure.storage.put('read-receipt-turn-on-asked', true); // this can be removed once enough people upgraded 8/12/2021 // Enable typing indicators by default - await window.textsecure.storage.put('typing-indicators-setting', Boolean(true)); + await window.textsecure.storage.put('typing-indicators-setting', true); await window.textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1); } diff --git a/yarn.lock b/yarn.lock index b66dbbada..654c33797 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1096,13 +1096,6 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== -"@types/mkdirp@0.5.2": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f" - integrity sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg== - dependencies: - "@types/node" "*" - "@types/mocha@5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.0.0.tgz#a3014921991066193f6c8e47290d4d598dfd19e6"