Merge branch 'clearnet' into theming

pull/2522/head
William Grant 3 years ago
commit 367c0c94d0

@ -56,4 +56,4 @@ Please visit https://deb.oxen.io/<br/>
Copyright 2011 Whisper Systems<br/>
Copyright 2013-2017 Open Whisper Systems<br/>
Copyright 2019-2021 The Oxen Project<br/>
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html<br/>
Licensed under the GPLv3: https://www.gnu.org/licenses/gpl-3.0.html<br/>

@ -363,6 +363,7 @@
"notificationPreview": "Preview",
"recoveryPhraseEmpty": "Enter your recovery phrase",
"displayNameEmpty": "Please enter a display name",
"displayNameTooLong": "Display name is too long",
"members": "$count$ members",
"activeMembers": "$count$ active members",
"join": "Join",

@ -49,6 +49,7 @@
"build-everything": "yarn clean && yarn protobuf && grunt && yarn sass && tsc && yarn parcel-util-worker",
"build-everything:watch": "yarn clean && yarn protobuf && grunt && yarn sass && yarn parcel-util-worker && tsc -w",
"watch": "yarn clean && yarn protobuf && grunt && concurrently 'yarn build-everything:watch' 'yarn sass:watch'",
"protobuf": "pbjs --target static-module --wrap commonjs --out ts/protobuf/compiled.js protos/*.proto && pbts --out ts/protobuf/compiled.d.ts ts/protobuf/compiled.js --force-long",
"sass": "rimraf 'stylesheets/dist/' && parcel build --target sass --no-autoinstall --no-cache",
@ -198,6 +199,7 @@
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-bytes": "^0.1.2",
"concurrently": "^7.4.0",
"cross-env": "^6.0.3",
"crypto-browserify": "^3.12.0",
"electron": "^17.2.0",

@ -740,11 +740,8 @@
flex-shrink: 1;
font-size: var(--font-size-sm);
line-height: 18px;
color: var(--conversation-tab-text-color);
height: 1.3em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;

@ -6,6 +6,21 @@
background: var(--background-primary-color);
}
.module-message__container {
.module-message__text {
font-size: 14px;
text-align: start;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
white-space: pre-wrap;
a {
text-decoration: underline;
}
}
}
.module-message {
position: relative;
display: inline-flex;

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import React, { useRef } from 'react';
import classNames from 'classnames';
import { SessionIconButton } from './icon/';
@ -63,17 +63,11 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => {
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, []);
return (
<div
className={classNames('loki-dialog modal', additionalClassName ? additionalClassName : null)}
onClick={handleClick}
role="dialog"
>
<div className="session-confirm-wrapper">
<div ref={modalRef} className="session-modal">

@ -2,8 +2,8 @@ import React from 'react';
import { Flex } from '../basic/Flex';
import styled from 'styled-components';
import { noop } from 'lodash';
import { SessionIcon, SessionIconType } from '../icon';
import { noop } from 'lodash';
// NOTE We don't change the color strip on the left based on the type. 16/09/2022
export enum SessionToastType {
@ -45,6 +45,8 @@ const IconDiv = styled.div`
margin: 0 var(--margins-xs);
`;
// tslint:disable: use-simple-attributes
export const SessionToast = (props: Props) => {
const { title, description, type, icon } = props;
@ -72,12 +74,14 @@ export const SessionToast = (props: Props) => {
}
}
const onToastClick = props?.onToastClick || noop;
return (
// tslint:disable-next-line: use-simple-attributes
<Flex
container={true}
alignItems="center"
onClick={props?.onToastClick || noop}
onClick={onToastClick}
data-testid="session-toast"
padding="var(--margins-sm) 0"
>

@ -15,12 +15,12 @@ import { uploadOurAvatar } from '../../interactions/conversationInteractions';
import { SessionButton, SessionButtonType } from '../basic/SessionButton';
import { SessionSpinner } from '../basic/SessionSpinner';
import { SessionIconButton } from '../icon';
import { MAX_USERNAME_LENGTH } from '../registration/RegistrationStages';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { pickFileForAvatar } from '../../types/attachments/VisualAttachment';
import { sanitizeSessionUsername } from '../../session/utils/String';
import { setLastProfileUpdateTimestamp } from '../../util/storage';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { MAX_USERNAME_BYTES } from '../../session/constants';
interface State {
profileName: string;
@ -211,7 +211,7 @@ export class EditProfileDialog extends React.Component<{}, State> {
value={this.state.profileName}
placeholder={placeholderText}
onChange={this.onNameEdited}
maxLength={MAX_USERNAME_LENGTH}
maxLength={MAX_USERNAME_BYTES}
tabIndex={0}
required={true}
aria-required={true}
@ -237,10 +237,18 @@ export class EditProfileDialog extends React.Component<{}, State> {
}
private onNameEdited(event: ChangeEvent<HTMLInputElement>) {
const newName = sanitizeSessionUsername(event.target.value);
this.setState({
profileName: newName,
});
const displayName = event.target.value;
try {
const newName = sanitizeSessionUsername(displayName);
this.setState({
profileName: newName,
});
} catch (e) {
this.setState({
profileName: displayName,
});
ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong'));
}
}
private onKeyUp(event: any) {
@ -263,26 +271,37 @@ export class EditProfileDialog extends React.Component<{}, State> {
*/
private onClickOK() {
const { newAvatarObjectUrl, profileName } = this.state;
const newName = profileName ? profileName.trim() : '';
try {
const newName = profileName ? profileName.trim() : '';
if (newName.length === 0 || newName.length > MAX_USERNAME_BYTES) {
return;
}
// this throw if the length in bytes is too long
const sanitizedName = sanitizeSessionUsername(newName);
const trimName = sanitizedName.trim();
this.setState(
{
profileName: trimName,
loading: true,
},
async () => {
await commitProfileEdits(newName, newAvatarObjectUrl);
this.setState({
loading: false,
mode: 'default',
updatedProfileName: this.state.profileName,
});
}
);
} catch (e) {
ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong'));
if (newName.length === 0 || newName.length > MAX_USERNAME_LENGTH) {
return;
}
this.setState(
{
loading: true,
},
async () => {
await commitProfileEdits(newName, newAvatarObjectUrl);
this.setState({
loading: false,
mode: 'default',
updatedProfileName: this.state.profileName,
});
}
);
}
private closeDialog() {

@ -39,7 +39,6 @@ import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { SessionIconButton } from '../icon';
import { SessionToastContainer } from '../SessionToastContainer';
import { LeftPaneSectionContainer } from './LeftPaneSectionContainer';
import { ipcRenderer } from 'electron';
import { UserUtils } from '../../session/utils';
@ -292,8 +291,6 @@ export const ActionsPanel = () => {
<Section type={SectionType.Message} />
<Section type={SectionType.Settings} />
<SessionToastContainer />
<Section type={SectionType.PathIndicator} />
<Section type={SectionType.ColorMode} />
</LeftPaneSectionContainer>

@ -12,6 +12,7 @@ import { CallInFullScreenContainer } from '../calling/CallInFullScreenContainer'
import { DraggableCallContainer } from '../calling/DraggableCallContainer';
import { IncomingCallDialog } from '../calling/IncomingCallDialog';
import { ModalContainer } from '../dialog/ModalContainer';
import { SessionToastContainer } from '../SessionToastContainer';
import { ActionsPanel } from './ActionsPanel';
import { LeftPaneMessageSection } from './LeftPaneMessageSection';
import { LeftPaneSettingSection } from './LeftPaneSettingSection';
@ -71,6 +72,7 @@ export const LeftPane = () => {
<div className="module-left-pane-session">
<ModalContainer />
<CallContainer />
<SessionToastContainer />
<ActionsPanel />
<StyledLeftPane className="module-left-pane">

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
// tslint:disable: use-simple-attributes no-submodule-imports
import { useDispatch } from 'react-redux';
@ -7,6 +7,7 @@ import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
import { SessionIcon, SessionIconType } from '../../../icon';
import { ContactsListWithBreaks } from './ContactsListWithBreaks';
import { isEmpty, isString } from 'lodash';
const StyledActionRow = styled.button`
border: none;
@ -45,7 +46,6 @@ const IconOnActionRow = (props: { iconType: SessionIconType }) => {
export const OverlayChooseAction = () => {
const dispatch = useDispatch();
function closeOverlay() {
dispatch(resetOverlayMode());
}
@ -64,6 +64,28 @@ export const OverlayChooseAction = () => {
useKey('Escape', closeOverlay);
function handlePaste(event: ClipboardEvent) {
event.preventDefault();
const pasted = event.clipboardData?.getData('text');
if (pasted && isString(pasted) && !isEmpty(pasted)) {
if (pasted.startsWith('http') || pasted.startsWith('https')) {
openJoinCommunity();
} else if (pasted.startsWith('05')) {
openNewMessage();
}
}
}
useEffect(() => {
document?.addEventListener('paste', handlePaste);
return () => {
document?.removeEventListener('paste', handlePaste);
};
}, []);
return (
<div className="module-left-pane-overlay">
<StyledActionRow

@ -17,7 +17,6 @@ import {
import { fromHex } from '../../session/utils/String';
import { setSignInByLinking, setSignWithRecoveryPhrase, Storage } from '../../util/storage';
export const MAX_USERNAME_LENGTH = 26;
// tslint:disable: use-simple-attributes
export async function resetRegistration() {

@ -1,7 +1,7 @@
import classNames from 'classnames';
import React from 'react';
import { MAX_USERNAME_BYTES } from '../../session/constants';
import { SessionInput } from '../basic/SessionInput';
import { MAX_USERNAME_LENGTH } from './RegistrationStages';
const DisplayNameInput = (props: {
stealAutoFocus?: boolean;
@ -17,7 +17,7 @@ const DisplayNameInput = (props: {
type="text"
placeholder={window.i18n('enterDisplayName')}
value={props.displayName}
maxLength={MAX_USERNAME_LENGTH}
maxLength={MAX_USERNAME_BYTES}
onValueChanged={props.onDisplayNameChanged}
onEnterPressed={props.handlePressEnter}
inputDataTestId="display-name-input"

@ -1,4 +1,5 @@
import React, { useContext, useState } from 'react';
import { ToastUtils } from '../../session/utils';
import { sanitizeSessionUsername } from '../../session/utils/String';
import { Flex } from '../basic/Flex';
import { SessionButton } from '../basic/SessionButton';
@ -89,6 +90,23 @@ const SignInButtons = (props: {
);
};
export function sanitizeDisplayNameOrToast(
displayName: string,
setDisplayName: (sanitized: string) => void,
setDisplayNameError: (error: string | undefined) => void
) {
try {
const sanitizedName = sanitizeSessionUsername(displayName);
const trimName = sanitizedName.trim();
setDisplayName(sanitizedName);
setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined);
} catch (e) {
setDisplayName(displayName);
setDisplayNameError(window.i18n('displayNameTooLong'));
ToastUtils.pushToastError('toolong', window.i18n('displayNameTooLong'));
}
}
export const SignInTab = () => {
const { setRegistrationPhase, signInMode, setSignInMode } = useContext(RegistrationContext);
@ -142,10 +160,7 @@ export const SignInTab = () => {
displayName={displayName}
handlePressEnter={continueYourSession}
onDisplayNameChanged={(name: string) => {
const sanitizedName = sanitizeSessionUsername(name);
const trimName = sanitizedName.trim();
setDisplayName(sanitizedName);
setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined);
sanitizeDisplayNameOrToast(name, setDisplayName, setDisplayNameError);
}}
onSeedChanged={(seed: string) => {
setRecoveryPhrase(seed);

@ -1,12 +1,11 @@
import React, { useContext, useEffect, useState } from 'react';
import { sanitizeSessionUsername } from '../../session/utils/String';
import { Flex } from '../basic/Flex';
import { SessionButton } from '../basic/SessionButton';
import { SessionIdEditable } from '../basic/SessionIdEditable';
import { SessionIconButton } from '../icon';
import { RegistrationContext, RegistrationPhase, signUp } from './RegistrationStages';
import { RegistrationUserDetails } from './RegistrationUserDetails';
import { SignInMode } from './SignInTab';
import { sanitizeDisplayNameOrToast, SignInMode } from './SignInTab';
import { TermsAndConditions } from './TermsAndConditions';
export enum SignUpMode {
@ -130,10 +129,7 @@ export const SignUpTab = () => {
displayName={displayName}
handlePressEnter={signUpWithDetails}
onDisplayNameChanged={(name: string) => {
const sanitizedName = sanitizeSessionUsername(name);
const trimName = sanitizedName.trim();
setDisplayName(sanitizedName);
setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined);
sanitizeDisplayNameOrToast(name, setDisplayName, setDisplayNameError);
}}
stealAutoFocus={true}
/>

@ -60,20 +60,17 @@ export const SessionNotificationGroupSettings = (props: { hasPassword: boolean |
if (!notificationsAreEnabled) {
return;
}
Notifications.addNotification(
{
conversationId: `preview-notification-${Date.now()}`,
message:
items.find(m => m.value === initialNotificationEnabled)?.label ||
window?.i18n?.('messageBody') ||
'Message body',
title: window.i18n('notificationPreview'),
iconUrl: null,
isExpiringMessage: false,
messageSentAt: Date.now(),
},
true
);
Notifications.addPreviewNotification({
conversationId: `preview-notification-${Date.now()}`,
message:
items.find(m => m.value === initialNotificationEnabled)?.label ||
window?.i18n?.('messageBody') ||
'Message body',
title: window.i18n('notificationPreview'),
iconUrl: null,
isExpiringMessage: false,
messageSentAt: Date.now(),
});
};
return (

@ -279,7 +279,13 @@ async function removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: string): P
// Conversation
async function saveConversation(data: ConversationAttributes): Promise<void> {
const cleaned = _cleanData(data);
/**
* Merging two conversations in `handleMessageRequestResponse` introduced a bug where we would mark conversation active_at to be -Infinity.
* The root issue has been fixed, but just to make sure those INVALID DATE does not show up, update those -Infinity active_at conversations to be now(), once.,
*/
if (cleaned.active_at === -Infinity) {
cleaned.active_at = Date.now();
}
await channels.saveConversation(cleaned);
}

@ -626,9 +626,13 @@ async function handleMessageRequestResponse(
unblindedConvoId,
ConversationTypeEnum.PRIVATE
);
const mostRecentActiveAt =
let mostRecentActiveAt =
Math.max(...compact(convosToMerge.map(m => m.get('active_at')))) || Date.now();
if (!isFinite(mostRecentActiveAt)) {
mostRecentActiveAt = Date.now();
}
conversationToApprove.set({
active_at: mostRecentActiveAt,
isApproved: true,

@ -77,10 +77,13 @@ export const downloadFileFromFileServer = async (
if (window.sessionFeatureFlags?.debug.debugFileServerRequests) {
window.log.info(`about to try to download fsv2: "${urlToGet}"`);
}
// this throws if we get a 404 from the file server
const result = await OnionSending.getBinaryViaOnionV4FromFileServer({
abortSignal: new AbortController().signal,
endpoint: urlToGet,
method: 'GET',
throwError: true,
});
if (window.sessionFeatureFlags?.debug.debugFileServerRequests) {
window.log.info(`download fsv2: "${urlToGet} got result:`, JSON.stringify(result));

@ -20,6 +20,7 @@ export async function fetchBinaryFromSogsWithOnionV4(sendOptions: {
headers: Record<string, any> | null;
roomId: string;
fileId: string;
throwError: boolean;
}): Promise<Uint8Array | null> {
const {
serverUrl,
@ -30,6 +31,7 @@ export async function fetchBinaryFromSogsWithOnionV4(sendOptions: {
doNotIncludeOurSogsHeaders,
roomId,
fileId,
throwError,
} = sendOptions;
const stringifiedBody = null;
@ -62,12 +64,12 @@ export async function fetchBinaryFromSogsWithOnionV4(sendOptions: {
body: stringifiedBody,
useV4: true,
},
false,
throwError,
abortSignal
);
if (!res?.bodyBinary) {
window.log.info('fetchBinaryFromSogsWithOnionV4 no binary content');
window.log.info('fetchBinaryFromSogsWithOnionV4 no binary content with code', res?.status_code);
return null;
}
return res.bodyBinary;
@ -169,6 +171,7 @@ const sogsV3FetchPreview = async (
doNotIncludeOurSogsHeaders: true,
roomId: roomInfos.roomId,
fileId: roomInfos.imageID,
throwError: false,
});
if (fetched && fetched.byteLength) {
return fetched;
@ -198,6 +201,7 @@ export const sogsV3FetchFileByFileID = async (
doNotIncludeOurSogsHeaders: true,
roomId: roomInfos.roomId,
fileId,
throwError: true,
});
return fetched && fetched.byteLength ? fetched : null;
};

@ -27,6 +27,14 @@ export const resetSnodeFailureCount = () => {
const snodeFailureThreshold = 3;
export const OXEN_SERVER_ERROR = 'Oxen Server error';
// Not ideal, but a pRetry.AbortError only lets us customize the message, and not the code
const errorContent404 = ': 404 ';
export const was404Error = (error: Error) => error.message.includes(errorContent404);
export const buildErrorMessageWithFailedCode = (prefix: string, code: number, suffix: string) =>
`${prefix}: ${code} ${suffix}`;
/**
* When sending a request over onion, we might get two status.
* The first one, on the request itself, the other one in the json returned.

@ -60,3 +60,5 @@ export const UI = {
export const QUOTED_TEXT_MAX_LENGTH = 150;
export const DEFAULT_RECENT_REACTS = ['😂', '🥰', '😢', '😡', '😮', '😈'];
export const MAX_USERNAME_BYTES = 64;

@ -2,6 +2,7 @@
import { OnionPaths } from '.';
import {
buildErrorMessageWithFailedCode,
FinalDestNonSnodeOptions,
FinalRelayOptions,
Onions,
@ -212,10 +213,17 @@ const sendViaOnionV4ToNonSnodeWithRetries = async (
};
}
if (foundStatusCode === 404) {
// this is most likely that a 404 won't fix itself. So just stop right here retries by throwing a non retryable error
throw new pRetry.AbortError(
window.log.warn(
`Got 404 while sendViaOnionV4ToNonSnodeWithRetries with url:${url}. Stopping retries`
);
// most likely, a 404 won't fix itself. So just stop right here retries by throwing a non retryable error
throw new pRetry.AbortError(
buildErrorMessageWithFailedCode(
'sendViaOnionV4ToNonSnodeWithRetries',
404,
`with url:${url}. Stopping retries`
)
);
}
// we consider those cases as an error, and trigger a retry (if possible), by throwing a non-abortable error
throw new Error(
@ -239,7 +247,7 @@ const sendViaOnionV4ToNonSnodeWithRetries = async (
}
);
} catch (e) {
window?.log?.warn('sendViaOnionV4ToNonSnodeRetryable failed ', e.message);
window?.log?.warn('sendViaOnionV4ToNonSnodeRetryable failed ', e.message, throwErrors);
if (throwErrors) {
throw e;
}
@ -455,8 +463,9 @@ async function getBinaryViaOnionV4FromFileServer(sendOptions: {
endpoint: string;
method: string;
abortSignal: AbortSignal;
throwError: boolean;
}): Promise<OnionV4BinarySnodeResponse | null> {
const { endpoint, method, abortSignal } = sendOptions;
const { endpoint, method, abortSignal, throwError } = sendOptions;
if (!endpoint.startsWith('/')) {
throw new Error('endpoint needs a leading /');
}
@ -465,6 +474,9 @@ async function getBinaryViaOnionV4FromFileServer(sendOptions: {
if (window.sessionFeatureFlags?.debug.debugFileServerRequests) {
window.log.info(`getBinaryViaOnionV4FromFileServer fsv2: "${builtUrl} `);
}
// this throws for a bunch of reasons.
// One of them, is if we get a 404 (i.e. the file server was reached but reported no such attachments exists)
const res = await OnionSending.sendViaOnionV4ToNonSnodeWithRetries(
fileServerPubKey,
builtUrl,
@ -474,7 +486,7 @@ async function getBinaryViaOnionV4FromFileServer(sendOptions: {
body: null,
useV4: true,
},
false,
throwError,
abortSignal
);

@ -8,6 +8,7 @@ import { MessageModel } from '../../models/message';
import { downloadAttachment, downloadAttachmentSogsV3 } from '../../receiver/attachments';
import { initializeAttachmentLogic, processNewAttachment } from '../../types/MessageAttachment';
import { getAttachmentMetadata } from '../../types/message/initializeAttachmentMetadata';
import { was404Error } from '../apis/snode_api/onions';
// this may cause issues if we increment that value to > 1, but only having one job will block the whole queue while one attachment is downloading
const MAX_ATTACHMENT_JOB_PARALLELISM = 3;
@ -175,6 +176,7 @@ async function _runJob(job: any) {
let downloaded;
try {
// those two functions throw if they get a 404
if (isOpenGroupV2) {
downloaded = await downloadAttachmentSogsV3(attachment, openGroupV2Details);
} else {
@ -189,14 +191,12 @@ async function _runJob(job: any) {
} from message ${found.idForLogging()} as permanent error`
);
await _finishJob(found, id);
// Make sure to fetch the message from DB here right before writing it.
// This is to avoid race condition where multiple attachments in a single message get downloaded at the same time,
// and tries to update the same message.
found = await Data.getMessageById(messageId);
_addAttachmentToMessage(found, _markAttachmentAsError(attachment), { type, index });
await _finishJob(found, id);
return;
}
@ -232,7 +232,9 @@ async function _runJob(job: any) {
// tslint:disable: restrict-plus-operands
const currentAttempt: 1 | 2 | 3 = (attempts || 0) + 1;
if (currentAttempt >= 3) {
// if we get a 404 error for attachment downloaded, we can safely assume that the attachment expired server-side.
// so there is no need to continue trying to download it.
if (currentAttempt >= 3 || was404Error(error)) {
logger.error(
`_runJob: ${currentAttempt} failed attempts, marking attachment ${id} from message ${found?.idForLogging()} as permament error:`,
error && error.stack ? error.stack : error
@ -321,40 +323,29 @@ function _addAttachmentToMessage(
return;
}
if (type === 'preview') {
// for quote and previews, if the attachment cannot be downloaded we just erase it from the message itself, so just the title or body is rendered
if (type === 'preview' || type === 'quote') {
if (type === 'quote') {
const quote = message.get('quote');
if (!quote) {
throw new Error("_addAttachmentToMessage: quote didn't exist");
}
delete message.attributes.quote.attachments;
return;
}
const preview = message.get('preview');
if (!preview || preview.length <= index) {
throw new Error(`_addAttachmentToMessage: preview didn't exist or ${index} was too large`);
}
const item = preview[index];
if (!item) {
throw new Error(`_addAttachmentToMessage: preview ${index} was falsey`);
}
_replaceAttachment(item, 'image', attachment, logPrefix);
return;
}
if (type === 'quote') {
const quote = message.get('quote');
if (!quote) {
throw new Error("_addAttachmentToMessage: quote didn't exist");
}
const { attachments } = quote;
if (!attachments || attachments.length <= index) {
throw new Error(
`_addAttachmentToMessage: quote attachments didn't exist or ${index} was too large`
);
}
const item = attachments[index];
if (!item) {
throw new Error(`_addAttachmentToMessage: attachment ${index} was falsey`);
}
_replaceAttachment(item, 'thumbnail', attachment, logPrefix);
delete message.attributes.preview[0].image;
return;
}
// for quote and previews, if the attachment cannot be downloaded we just erase it from the message itself, so just the title or body is rendered
throw new Error(
`_addAttachmentToMessage: Unknown job type ${type} for message ${message.idForLogging()}`
);

@ -1,4 +1,5 @@
import ByteBuffer from 'bytebuffer';
import { MAX_USERNAME_BYTES } from '../constants';
export type Encoding = 'base64' | 'hex' | 'binary' | 'utf8';
export type BufferType = ByteBuffer | Buffer | ArrayBuffer | Uint8Array;
@ -54,10 +55,19 @@ const forbiddenDisplayCharRegex = /\uFFD2*/g;
*
* This function removes any forbidden char from a given display name.
* This does not trim it as otherwise, a user cannot type User A as when he hits the space, it gets trimmed right away.
* The trimming should hence happen after calling this and on saving the display name
* The trimming should hence happen after calling this and on saving the display name.
*
* This functions makes sure that the MAX_USERNAME_BYTES is verified for utf8 byte length
* @param inputName the input to sanitize
* @returns a sanitized string, untrimmed
*/
export const sanitizeSessionUsername = (inputName: string) => {
return inputName.replace(forbiddenDisplayCharRegex, '');
const validChars = inputName.replace(forbiddenDisplayCharRegex, '');
const lengthBytes = encode(validChars, 'utf8').byteLength;
if (lengthBytes > MAX_USERNAME_BYTES) {
throw new Error('Display name is too long');
}
return validChars;
};

@ -495,6 +495,7 @@ export type LocalizerKeys =
| 'trustThisContactDialogDescription'
| 'unknownCountry'
| 'searchFor...'
| 'displayNameTooLong'
| 'joinedTheGroup'
| 'editGroupName'
| 'reportIssue';

@ -1,4 +1,5 @@
import { Data } from '../data/data';
import { getConversationController } from '../session/conversations';
import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils';
@ -105,6 +106,13 @@ export class BlockedNumberController {
}
});
users.map(user => {
const found = getConversationController().get(user);
if (found) {
found.triggerUIRefresh();
}
});
if (changes) {
await this.saveToDB(BLOCKED_NUMBERS_ID, this.blockedNumbers);
}

@ -77,7 +77,7 @@ function disable() {
*
* @param forceRefresh Should only be set when the user triggers a test notification from the settings
*/
function addNotification(notif: SessionNotification, forceRefresh = false) {
function addNotification(notif: SessionNotification) {
const alreadyThere = currentNotifications.find(
n => n.conversationId === notif.conversationId && n.messageId === notif.messageId
);
@ -86,11 +86,15 @@ function addNotification(notif: SessionNotification, forceRefresh = false) {
return;
}
currentNotifications.push(notif);
if (forceRefresh) {
update(true);
} else {
debouncedUpdate();
}
debouncedUpdate();
}
/**
* Special case when we want to display a preview of what notifications looks like
*/
function addPreviewNotification(notif: SessionNotification) {
currentNotifications.push(notif);
update(true);
}
function clearByConversationID(convoId: string) {
@ -131,7 +135,7 @@ function update(forceRefresh = false) {
isAppFocused: forceRefresh ? false : isAppFocused,
isAudioNotificationEnabled,
isAudioNotificationSupported: audioNotificationSupported,
isEnabled: forceRefresh ? true : isEnabled,
isEnabled,
numNotifications,
userSetting,
});
@ -251,6 +255,7 @@ function onRemove() {
export const Notifications = {
addNotification,
addPreviewNotification,
disable,
enable,
clear,

@ -3060,6 +3060,15 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
clone-response@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
@ -3199,6 +3208,21 @@ concat-stream@^1.6.2:
readable-stream "^2.2.2"
typedarray "^0.0.6"
concurrently@^7.4.0:
version "7.4.0"
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.4.0.tgz#bb0e344964bc172673577c420db21e963f2f7368"
integrity sha512-M6AfrueDt/GEna/Vg9BqQ+93yuvzkSKmoTixnwEJkH0LlcGrRC2eCmjeG1tLLHIYfpYJABokqSGyMcXjm96AFA==
dependencies:
chalk "^4.1.0"
date-fns "^2.29.1"
lodash "^4.17.21"
rxjs "^7.0.0"
shell-quote "^1.7.3"
spawn-command "^0.0.2-1"
supports-color "^8.1.0"
tree-kill "^1.2.2"
yargs "^17.3.1"
config-chain@^1.1.11:
version "1.1.13"
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4"
@ -3458,6 +3482,11 @@ data-urls@^3.0.1:
whatwg-mimetype "^3.0.0"
whatwg-url "^11.0.0"
date-fns@^2.29.1:
version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
dateformat@~3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
@ -6146,7 +6175,7 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.20, lodash@^4.2.1, lodash@~4.17.10, lodash@~4.17.19, lodash@~4.17.21:
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.2.1, lodash@~4.17.10, lodash@~4.17.19, lodash@~4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -8020,6 +8049,13 @@ run-script-os@^1.1.6:
resolved "https://registry.yarnpkg.com/run-script-os/-/run-script-os-1.1.6.tgz#8b0177fb1b54c99a670f95c7fdc54f18b9c72347"
integrity sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==
rxjs@^7.0.0:
version "7.5.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39"
integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==
dependencies:
tslib "^2.1.0"
safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@ -8186,6 +8222,11 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
@ -8299,6 +8340,11 @@ sourcemap-codec@^1.4.8:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spawn-command@^0.0.2-1:
version "0.0.2-1"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==
spawn-wrap@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-2.0.0.tgz#103685b8b8f9b79771318827aa78650a610d457e"
@ -8534,7 +8580,7 @@ sumchecker@^3.0.1:
dependencies:
debug "^4.1.0"
supports-color@8.1.1:
supports-color@8.1.1, supports-color@^8.1.0:
version "8.1.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
@ -8716,6 +8762,11 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
truncate-utf8-bytes@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
@ -9266,6 +9317,11 @@ yargs-parser@^20.2.2:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs-parser@^21.0.0:
version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs-unparser@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
@ -9306,6 +9362,19 @@ yargs@^15.0.2, yargs@^15.3.1:
y18n "^4.0.0"
yargs-parser "^18.1.2"
yargs@^17.3.1:
version "17.6.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.0.tgz#e134900fc1f218bc230192bdec06a0a5f973e46c"
integrity sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==
dependencies:
cliui "^8.0.1"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.3"
y18n "^5.0.5"
yargs-parser "^21.0.0"
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"

Loading…
Cancel
Save