Minor call tweaks (#2051)
* show missed-call,started-call and answered call notification in chat * fix types for createLastMessageUpdate * show incoming dialog if we have a pending call when enable call receptio * simplify a bit the avatar component * move disableDrag to a custom hook * speed up hash colors of avatarPlaceHolders * fixup text selection and double click reply on message * keep avatar decoded items longer before releasing memory * add incoming/outgoing/missed call notification also, merge that notification with the timer and group notification component * hangup call if no answer after 30sec * refactor SessionInput using hook + add testid field for recovery * disable message request feature flag for now * fix merge issue * force loading screen to be black instead of white for our dark theme user's eyes safety
This commit is contained in:
parent
56d58a35e5
commit
cf44896a03
|
@ -460,5 +460,7 @@
|
|||
"callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.",
|
||||
"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"
|
||||
"menuCall": "Call",
|
||||
"startedACall": "You called $name$",
|
||||
"answeredACall": "Call with $name$"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* global crypto */
|
||||
|
||||
const { isFunction } = require('lodash');
|
||||
const { createLastMessageUpdate } = require('../../../ts/types/Conversation');
|
||||
const { arrayBufferToBase64 } = require('../crypto');
|
||||
|
||||
async function computeHash(arraybuffer) {
|
||||
|
@ -80,6 +79,5 @@ async function deleteExternalFiles(conversation, options = {}) {
|
|||
module.exports = {
|
||||
deleteExternalFiles,
|
||||
maybeUpdateAvatar,
|
||||
createLastMessageUpdate,
|
||||
arrayBufferToBase64,
|
||||
};
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
"integration-test": "mocha --recursive --exit --timeout 30000 \"./ts/test-integration/**/*.test.js\" \"./ts/test/*.test.js\"",
|
||||
"clean-transpile": "rimraf 'ts/**/*.js' 'ts/*.js' 'ts/*.js.map' 'ts/**/*.js.map' && rimraf tsconfig.tsbuildinfo;",
|
||||
"ready": "yarn clean-transpile; yarn grunt && yarn lint-full && yarn test",
|
||||
"build:webpack:sql-worker": "cross-env NODE_ENV=production webpack -c webpack-sql-worker.config.ts",
|
||||
"sedtoAppImage": "sed -i 's/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/\"target\": \"AppImage\"/g' package.json",
|
||||
"sedtoDeb": "sed -i 's/\"target\": \"AppImage\"/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/g' package.json"
|
||||
},
|
||||
|
|
|
@ -39,7 +39,7 @@ window.isBehindProxy = () => Boolean(config.proxyUrl);
|
|||
|
||||
window.lokiFeatureFlags = {
|
||||
useOnionRequests: true,
|
||||
useMessageRequests: true,
|
||||
useMessageRequests: false,
|
||||
useCallMessage: true,
|
||||
};
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ body {
|
|||
margin: 0;
|
||||
font-family: $session-font-default;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
// scrollbars
|
||||
|
@ -230,6 +231,8 @@ $loading-height: 16px;
|
|||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
// force this to black, to stay consistent with the password prompt being in dark mode too.
|
||||
background-color: black;
|
||||
|
||||
.content {
|
||||
margin-inline-start: auto;
|
||||
|
|
|
@ -276,185 +276,6 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.module-message__typing-container {
|
||||
height: 16px;
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Module: Contact Detail
|
||||
|
||||
.module-contact-detail {
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
.module-contact-detail__avatar {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.module-contact-detail__contact-name {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.module-contact-detail__contact-method {
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.module-contact-detail__send-message {
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 4px;
|
||||
background-color: $blue;
|
||||
display: inline-block;
|
||||
padding: 6px;
|
||||
margin-top: 20px;
|
||||
|
||||
color: $color-white;
|
||||
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
@include button-reset;
|
||||
}
|
||||
}
|
||||
|
||||
.module-contact-detail__send-message__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.module-contact-detail__additional-contact {
|
||||
text-align: left;
|
||||
border-top: 1px solid $color-light-10;
|
||||
margin-top: 15px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.module-contact-detail__additional-contact__type {
|
||||
color: $color-light-45;
|
||||
font-size: 12px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
// Module: Group Notification
|
||||
|
||||
.module-group-notification {
|
||||
margin-top: 14px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.3px;
|
||||
color: $color-gray-60;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.module-group-notification__change,
|
||||
.module-timer-notification__message {
|
||||
background: var(--color-fake-chat-bubble-background);
|
||||
color: var(--color-text);
|
||||
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
margin: 10px auto;
|
||||
padding: 5px 0px;
|
||||
border-radius: 4px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.module-group-notification__contact {
|
||||
font-family: $session-font-default;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Module: Timer Notification
|
||||
|
||||
.module-timer-notification {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.3px;
|
||||
color: $color-gray-60;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.module-timer-notification__contact {
|
||||
font-family: $session-font-default;
|
||||
font-weight: bold;
|
||||
padding-inline-end: $session-margin-xs;
|
||||
}
|
||||
|
||||
.module-timer-notification__icon-container {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.module-timer-notification__icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
@include color-svg('../images/timer.svg', $color-gray-60);
|
||||
}
|
||||
|
||||
.module-timer-notification__icon--disabled {
|
||||
@include color-svg('../images/timer-disabled.svg', $color-gray-60);
|
||||
}
|
||||
|
||||
.module-timer-notification__icon-label {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.3px;
|
||||
margin-inline-start: 6px;
|
||||
text-transform: uppercase;
|
||||
|
||||
// Didn't seem centered otherwise
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.module-timer-notification__message {
|
||||
display: grid;
|
||||
grid-template-columns: 40px auto 40px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
& > div {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.module-contact-name__profile-name {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.module-message__author__profile-name {
|
||||
margin-inline-end: $session-margin-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.module-notification--with-click-handler {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.module-notification__icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
// Module: Contact List Item
|
||||
|
||||
.module-contact-list-item {
|
||||
|
@ -549,10 +370,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.module-conversation-header__title__profile-name {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.module-conversation-header__expiration {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -269,7 +269,6 @@ textarea {
|
|||
|
||||
.module-conversation-header__title-flex,
|
||||
.module-conversation-header__title {
|
||||
font-family: $session-font-accent;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
@ -278,7 +277,6 @@ textarea {
|
|||
&-text {
|
||||
@include session-color-subtle(var(--color-text));
|
||||
|
||||
font-family: $session-font-default;
|
||||
font-weight: 300;
|
||||
font-size: $session-font-sm;
|
||||
line-height: $session-font-sm;
|
||||
|
|
|
@ -225,26 +225,6 @@
|
|||
color: $color-light-45;
|
||||
}
|
||||
|
||||
// Module: Group Notification
|
||||
|
||||
.module-group-notification {
|
||||
color: $color-dark-30;
|
||||
}
|
||||
|
||||
// Module: Timer Notification
|
||||
|
||||
.module-timer-notification {
|
||||
color: $color-dark-30;
|
||||
}
|
||||
|
||||
.module-timer-notification__icon {
|
||||
@include color-svg('../images/timer.svg', $color-dark-30);
|
||||
}
|
||||
|
||||
.module-timer-notification__icon--disabled {
|
||||
@include color-svg('../images/timer-disabled.svg', $color-dark-30);
|
||||
}
|
||||
|
||||
// Module: Contact List Item
|
||||
|
||||
.module-contact-list-item {
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { AvatarPlaceHolder, ClosedGroupAvatar } from './AvatarPlaceHolder';
|
||||
import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch';
|
||||
import _ from 'underscore';
|
||||
import { useMembersAvatars } from '../hooks/useMembersAvatars';
|
||||
import { useAvatarPath, useConversationUsername } from '../hooks/useParamSelector';
|
||||
import {
|
||||
useAvatarPath,
|
||||
useConversationUsername,
|
||||
useIsClosedGroup,
|
||||
} from '../hooks/useParamSelector';
|
||||
import { AvatarPlaceHolder } from './AvatarPlaceHolder/AvatarPlaceHolder';
|
||||
import { ClosedGroupAvatar } from './AvatarPlaceHolder/ClosedGroupAvatar';
|
||||
import { useDisableDrag } from '../hooks/useDisableDrag';
|
||||
|
||||
export enum AvatarSize {
|
||||
XS = 28,
|
||||
|
@ -19,7 +23,7 @@ export enum AvatarSize {
|
|||
type Props = {
|
||||
forcedAvatarPath?: string | null;
|
||||
forcedName?: string;
|
||||
pubkey?: string;
|
||||
pubkey: string;
|
||||
size: AvatarSize;
|
||||
base64Data?: string; // if this is not empty, it will be used to render the avatar with base64 encoded data
|
||||
onAvatarClick?: () => void;
|
||||
|
@ -28,17 +32,10 @@ type Props = {
|
|||
|
||||
const Identicon = (props: Props) => {
|
||||
const { size, forcedName, pubkey } = props;
|
||||
const userName = forcedName || '0';
|
||||
const displayName = useConversationUsername(pubkey);
|
||||
const userName = forcedName || displayName || '0';
|
||||
|
||||
return (
|
||||
<AvatarPlaceHolder
|
||||
diameter={size}
|
||||
name={userName}
|
||||
pubkey={pubkey}
|
||||
colors={['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a']}
|
||||
borderColor={'#00000059'}
|
||||
/>
|
||||
);
|
||||
return <AvatarPlaceHolder diameter={size} name={userName} pubkey={pubkey} />;
|
||||
};
|
||||
|
||||
const NoImage = (
|
||||
|
@ -66,10 +63,7 @@ const AvatarImage = (props: {
|
|||
}) => {
|
||||
const { avatarPath, base64Data, name, imageBroken, handleImageError } = props;
|
||||
|
||||
const onDragStart = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, []);
|
||||
const disableDrag = useDisableDrag();
|
||||
|
||||
if ((!avatarPath && !base64Data) || imageBroken) {
|
||||
return null;
|
||||
|
@ -79,7 +73,7 @@ const AvatarImage = (props: {
|
|||
return (
|
||||
<img
|
||||
onError={handleImageError}
|
||||
onDragStart={onDragStart}
|
||||
onDragStart={disableDrag}
|
||||
alt={window.i18n('contactAvatarAlt', [name])}
|
||||
src={dataToDisplay}
|
||||
/>
|
||||
|
@ -90,13 +84,13 @@ const AvatarInner = (props: Props) => {
|
|||
const { base64Data, size, pubkey, forcedAvatarPath, forcedName, dataTestId } = props;
|
||||
const [imageBroken, setImageBroken] = useState(false);
|
||||
|
||||
const closedGroupMembers = useMembersAvatars(pubkey);
|
||||
const isClosedGroupAvatar = useIsClosedGroup(pubkey);
|
||||
|
||||
const avatarPath = useAvatarPath(pubkey);
|
||||
const name = useConversationUsername(pubkey);
|
||||
|
||||
// contentType is not important
|
||||
const { urlToLoad } = useEncryptedFileFetch(forcedAvatarPath || avatarPath || '', '');
|
||||
const { urlToLoad } = useEncryptedFileFetch(forcedAvatarPath || avatarPath || '', '', true);
|
||||
const handleImageError = () => {
|
||||
window.log.warn(
|
||||
'Avatar: Image failed to load; failing over to placeholder',
|
||||
|
@ -106,7 +100,6 @@ const AvatarInner = (props: Props) => {
|
|||
setImageBroken(true);
|
||||
};
|
||||
|
||||
const isClosedGroupAvatar = Boolean(closedGroupMembers?.length);
|
||||
const hasImage = (base64Data || urlToLoad) && !imageBroken && !isClosedGroupAvatar;
|
||||
|
||||
const isClickable = !!props.onAvatarClick;
|
||||
|
|
|
@ -4,13 +4,10 @@ import { getInitials } from '../../util/getInitials';
|
|||
type Props = {
|
||||
diameter: number;
|
||||
name: string;
|
||||
pubkey?: string;
|
||||
colors: Array<string>;
|
||||
borderColor: string;
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
const sha512FromPubkey = async (pubkey: string): Promise<string> => {
|
||||
// tslint:disable-next-line: await-promise
|
||||
const buf = await crypto.subtle.digest('SHA-512', new TextEncoder().encode(pubkey));
|
||||
|
||||
// tslint:disable: prefer-template restrict-plus-operands
|
||||
|
@ -19,34 +16,69 @@ const sha512FromPubkey = async (pubkey: string): Promise<string> => {
|
|||
.join('');
|
||||
};
|
||||
|
||||
export const AvatarPlaceHolder = (props: Props) => {
|
||||
const { borderColor, colors, pubkey, diameter, name } = props;
|
||||
const [sha512Seed, setSha512Seed] = useState(undefined as string | undefined);
|
||||
// do not do this on every avatar, just cache the values so we can reuse them accross the app
|
||||
// key is the pubkey, value is the hash
|
||||
const cachedHashes = new Map<string, number>();
|
||||
|
||||
const avatarPlaceholderColors = ['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a'];
|
||||
const avatarBorderColor = '#00000059';
|
||||
|
||||
function useHashBasedOnPubkey(pubkey: string) {
|
||||
const [hash, setHash] = useState<number | undefined>(undefined);
|
||||
const [loading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const cachedHash = cachedHashes.get(pubkey);
|
||||
|
||||
if (cachedHash) {
|
||||
setHash(cachedHash);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
let isInProgress = true;
|
||||
|
||||
if (!pubkey) {
|
||||
if (isSubscribed) {
|
||||
setSha512Seed(undefined);
|
||||
if (isInProgress) {
|
||||
setIsLoading(false);
|
||||
|
||||
setHash(undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
void sha512FromPubkey(pubkey).then(sha => {
|
||||
if (isSubscribed) {
|
||||
setSha512Seed(sha);
|
||||
if (isInProgress) {
|
||||
setIsLoading(false);
|
||||
// Generate the seed simulate the .hashCode as Java
|
||||
if (sha) {
|
||||
const hashed = parseInt(sha.substring(0, 12), 16) || 0;
|
||||
setHash(hashed);
|
||||
cachedHashes.set(pubkey, hashed);
|
||||
|
||||
return;
|
||||
}
|
||||
setHash(undefined);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
isInProgress = false;
|
||||
};
|
||||
}, [pubkey, name]);
|
||||
}, [pubkey]);
|
||||
|
||||
return { loading, hash };
|
||||
}
|
||||
|
||||
export const AvatarPlaceHolder = (props: Props) => {
|
||||
const { pubkey, diameter, name } = props;
|
||||
|
||||
const { hash, loading } = useHashBasedOnPubkey(pubkey);
|
||||
|
||||
const diameterWithoutBorder = diameter - 2;
|
||||
const viewBox = `0 0 ${diameter} ${diameter}`;
|
||||
const r = diameter / 2;
|
||||
const rWithoutBorder = diameterWithoutBorder / 2;
|
||||
|
||||
if (!sha512Seed) {
|
||||
if (loading || !hash) {
|
||||
// return grey circle
|
||||
return (
|
||||
<svg viewBox={viewBox}>
|
||||
|
@ -57,7 +89,7 @@ export const AvatarPlaceHolder = (props: Props) => {
|
|||
r={rWithoutBorder}
|
||||
fill="#d2d2d3"
|
||||
shapeRendering="geometricPrecision"
|
||||
stroke={borderColor}
|
||||
stroke={avatarBorderColor}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</g>
|
||||
|
@ -68,12 +100,9 @@ export const AvatarPlaceHolder = (props: Props) => {
|
|||
const initial = getInitials(name)?.toLocaleUpperCase() || '0';
|
||||
const fontSize = diameter * 0.5;
|
||||
|
||||
// Generate the seed simulate the .hashCode as Java
|
||||
const hash = parseInt(sha512Seed.substring(0, 12), 16) || 0;
|
||||
const bgColorIndex = hash % avatarPlaceholderColors.length;
|
||||
|
||||
const bgColorIndex = hash % colors.length;
|
||||
|
||||
const bgColor = colors[bgColorIndex];
|
||||
const bgColor = avatarPlaceholderColors[bgColorIndex];
|
||||
|
||||
return (
|
||||
<svg viewBox={viewBox}>
|
||||
|
@ -84,7 +113,7 @@ export const AvatarPlaceHolder = (props: Props) => {
|
|||
r={rWithoutBorder}
|
||||
fill={bgColor}
|
||||
shapeRendering="geometricPrecision"
|
||||
stroke={borderColor}
|
||||
stroke={avatarBorderColor}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<text
|
||||
|
|
|
@ -36,8 +36,8 @@ export const ClosedGroupAvatar = (props: Props) => {
|
|||
|
||||
return (
|
||||
<div className="module-avatar__icon-closed">
|
||||
<Avatar size={avatarsDiameter} pubkey={firstMemberId} onAvatarClick={onAvatarClick} />
|
||||
<Avatar size={avatarsDiameter} pubkey={secondMemberID} onAvatarClick={onAvatarClick} />
|
||||
<Avatar size={avatarsDiameter} pubkey={firstMemberId || ''} onAvatarClick={onAvatarClick} />
|
||||
<Avatar size={avatarsDiameter} pubkey={secondMemberID || ''} onAvatarClick={onAvatarClick} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export { AvatarPlaceHolder } from './AvatarPlaceHolder';
|
||||
export { ClosedGroupAvatar } from './ClosedGroupAvatar';
|
|
@ -20,7 +20,7 @@ import {
|
|||
ReduxConversationType,
|
||||
} from '../state/ducks/conversations';
|
||||
import _ from 'underscore';
|
||||
import { SessionIcon } from './session/icon';
|
||||
import { SessionIcon, SessionIconButton } from './session/icon';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SectionType } from '../state/ducks/section';
|
||||
import { getFocusedSection } from '../state/selectors/section';
|
||||
|
@ -83,7 +83,7 @@ const HeaderItem = (props: {
|
|||
|
||||
const pinIcon =
|
||||
isMessagesSection && isPinned ? (
|
||||
<SessionIcon iconType="pin" iconColor={'var(--color-text-subtle)'} iconSize={'small'} />
|
||||
<SessionIcon iconType="pin" iconColor={'var(--color-text-subtle)'} iconSize="small" />
|
||||
) : null;
|
||||
|
||||
const NotificationSettingIcon = () => {
|
||||
|
@ -96,11 +96,11 @@ const HeaderItem = (props: {
|
|||
return null;
|
||||
case 'disabled':
|
||||
return (
|
||||
<SessionIcon iconType="mute" iconColor={'var(--color-text-subtle)'} iconSize={'small'} />
|
||||
<SessionIcon iconType="mute" iconColor={'var(--color-text-subtle)'} iconSize="small" />
|
||||
);
|
||||
case 'mentions_only':
|
||||
return (
|
||||
<SessionIcon iconType="bell" iconColor={'var(--color-text-subtle)'} iconSize={'small'} />
|
||||
<SessionIcon iconType="bell" iconColor={'var(--color-text-subtle)'} iconSize="small" />
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { RenderTextCallbackType } from '../types/Util';
|
||||
|
||||
type FullJSX = Array<JSX.Element | string> | JSX.Element | string;
|
||||
|
||||
interface Props {
|
||||
/** The translation string id */
|
||||
id: string;
|
||||
components?: Array<FullJSX>;
|
||||
renderText?: RenderTextCallbackType;
|
||||
}
|
||||
|
||||
export class Intl extends React.Component<Props> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
renderText: ({ text }) => text,
|
||||
};
|
||||
|
||||
public getComponent(index: number): FullJSX | undefined {
|
||||
const { id, components } = this.props;
|
||||
|
||||
if (!components || !components.length || components.length <= index) {
|
||||
// tslint:disable-next-line no-console
|
||||
console.log(`Error: Intl missing provided components for id ${id}, index ${index}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return components[index];
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { id, renderText } = this.props;
|
||||
|
||||
const text = window.i18n(id);
|
||||
const results: Array<any> = [];
|
||||
const FIND_REPLACEMENTS = /\$[^$]+\$/g;
|
||||
|
||||
// We have to do this, because renderText is not required in our Props object,
|
||||
// but it is always provided via defaultProps.
|
||||
if (!renderText) {
|
||||
return;
|
||||
}
|
||||
|
||||
let componentIndex = 0;
|
||||
let key = 0;
|
||||
let lastTextIndex = 0;
|
||||
let match = FIND_REPLACEMENTS.exec(text);
|
||||
|
||||
if (!match) {
|
||||
return renderText({ text, key: 0 });
|
||||
}
|
||||
|
||||
while (match) {
|
||||
if (lastTextIndex < match.index) {
|
||||
const textWithNoReplacements = text.slice(lastTextIndex, match.index);
|
||||
results.push(renderText({ text: textWithNoReplacements, key: key }));
|
||||
key += 1;
|
||||
}
|
||||
|
||||
results.push(this.getComponent(componentIndex));
|
||||
componentIndex += 1;
|
||||
|
||||
// @ts-ignore
|
||||
lastTextIndex = FIND_REPLACEMENTS.lastIndex;
|
||||
match = FIND_REPLACEMENTS.exec(text);
|
||||
}
|
||||
|
||||
if (lastTextIndex < text.length) {
|
||||
results.push(renderText({ text: text.slice(lastTextIndex), key: key }));
|
||||
key += 1;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// tslint:disable:react-a11y-anchors
|
||||
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import is from '@sindresorhus/is';
|
||||
|
||||
|
@ -14,6 +14,7 @@ import useUnmount from 'react-use/lib/useUnmount';
|
|||
import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { showLightBox } from '../state/ducks/conversations';
|
||||
import { useDisableDrag } from '../hooks/useDisableDrag';
|
||||
|
||||
const Colors = {
|
||||
TEXT_SECONDARY: '#bbb',
|
||||
|
@ -204,15 +205,10 @@ export const LightboxObject = ({
|
|||
renderedRef: React.MutableRefObject<any>;
|
||||
onObjectClick: (event: any) => any;
|
||||
}) => {
|
||||
const { urlToLoad } = useEncryptedFileFetch(objectURL, contentType);
|
||||
const { urlToLoad } = useEncryptedFileFetch(objectURL, contentType, false);
|
||||
|
||||
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
|
||||
|
||||
const onDragStart = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
// auto play video on showing a video attachment
|
||||
useUnmount(() => {
|
||||
if (!renderedRef?.current) {
|
||||
|
@ -220,12 +216,13 @@ export const LightboxObject = ({
|
|||
}
|
||||
renderedRef.current.pause.pause();
|
||||
});
|
||||
const disableDrag = useDisableDrag();
|
||||
|
||||
if (isImageTypeSupported) {
|
||||
return (
|
||||
<img
|
||||
style={styles.object as any}
|
||||
onDragStart={onDragStart}
|
||||
onDragStart={disableDrag}
|
||||
alt={window.i18n('lightboxImageAlt')}
|
||||
src={urlToLoad}
|
||||
ref={renderedRef}
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
} from '../../state/ducks/conversations';
|
||||
import { callRecipient } from '../../interactions/conversationInteractions';
|
||||
import { getHasIncomingCall, getHasOngoingCall } from '../../state/selectors/call';
|
||||
import { useConversationUsername } from '../../hooks/useParamSelector';
|
||||
|
||||
export interface TimerOption {
|
||||
name: string;
|
||||
|
@ -191,7 +192,7 @@ const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) =>
|
|||
}
|
||||
|
||||
return (
|
||||
<SessionIconButton iconType="chevron" iconSize={'large'} iconRotation={90} onClick={onGoBack} />
|
||||
<SessionIconButton iconType="chevron" iconSize="large" iconRotation={90} onClick={onGoBack} />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -249,22 +250,14 @@ const ConversationHeaderTitle = () => {
|
|||
const headerTitleProps = useSelector(getConversationHeaderTitleProps);
|
||||
const notificationSetting = useSelector(getCurrentNotificationSettingText);
|
||||
const isRightPanelOn = useSelector(isRightPanelShowing);
|
||||
|
||||
const convoName = useConversationUsername(headerTitleProps?.conversationKey);
|
||||
const dispatch = useDispatch();
|
||||
if (!headerTitleProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
conversationKey,
|
||||
profileName,
|
||||
isGroup,
|
||||
isPublic,
|
||||
members,
|
||||
subscriberCount,
|
||||
isMe,
|
||||
isKickedFromGroup,
|
||||
name,
|
||||
} = headerTitleProps;
|
||||
const { isGroup, isPublic, members, subscriberCount, isMe, isKickedFromGroup } = headerTitleProps;
|
||||
|
||||
const { i18n } = window;
|
||||
|
||||
|
@ -294,8 +287,6 @@ const ConversationHeaderTitle = () => {
|
|||
? `${memberCountText} ● ${notificationSubtitle}`
|
||||
: `${notificationSubtitle}`;
|
||||
|
||||
const title = profileName || name || conversationKey;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="module-conversation-header__title"
|
||||
|
@ -308,7 +299,7 @@ const ConversationHeaderTitle = () => {
|
|||
}}
|
||||
role="button"
|
||||
>
|
||||
<span className="module-contact-name__profile-name">{title}</span>
|
||||
<span className="module-contact-name__profile-name">{convoName}</span>
|
||||
<StyledSubtitleContainer>
|
||||
<ConversationHeaderSubtitle text={fullTextSubtitle} />
|
||||
</StyledSubtitleContainer>
|
||||
|
|
|
@ -31,7 +31,7 @@ export const DataExtractionNotification = (props: PropsForDataExtractionNotifica
|
|||
margin={'var(--margins-sm)'}
|
||||
id={`msg-${messageId}`}
|
||||
>
|
||||
<SessionIcon iconType="upload" iconSize={'small'} iconRotation={180} />
|
||||
<SessionIcon iconType="upload" iconSize="small" iconRotation={180} />
|
||||
<SpacerSM />
|
||||
<Text text={contentText} subtle={true} />
|
||||
</Flex>
|
||||
|
|
|
@ -64,7 +64,7 @@ export const ExpireTimer = (props: Props) => {
|
|||
|
||||
return (
|
||||
<ExpireTimerBucket>
|
||||
<SessionIcon iconType={bucket} iconSize={'tiny'} iconColor={expireTimerColor} />
|
||||
<SessionIcon iconType={bucket} iconSize="tiny" iconColor={expireTimerColor} />
|
||||
</ExpireTimerBucket>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
import { flatten } from 'lodash';
|
||||
|
||||
import { Intl } from '../Intl';
|
||||
import {
|
||||
PropsForGroupUpdate,
|
||||
PropsForGroupUpdateAdd,
|
||||
|
@ -11,6 +9,7 @@ import {
|
|||
} from '../../state/ducks/conversations';
|
||||
import _ from 'underscore';
|
||||
import { ReadableMessage } from './ReadableMessage';
|
||||
import { NotificationBubble } from './notification-bubble/NotificationBubble';
|
||||
|
||||
// This component is used to display group updates in the conversation view.
|
||||
// This is a not a "notification" as the name suggests, but a message inside the conversation
|
||||
|
@ -25,34 +24,22 @@ function isTypeWithContact(change: PropsForGroupUpdateType): change is TypeWithC
|
|||
}
|
||||
|
||||
function getPeople(change: TypeWithContacts) {
|
||||
return _.compact(
|
||||
flatten(
|
||||
(change.contacts || []).map((contact, index) => {
|
||||
const element = (
|
||||
<span key={`external-${contact.pubkey}`} className="module-group-notification__contact">
|
||||
{contact.profileName || contact.pubkey}
|
||||
</span>
|
||||
);
|
||||
|
||||
return [index > 0 ? ', ' : null, element];
|
||||
})
|
||||
)
|
||||
);
|
||||
return change.contacts?.map(c => c.profileName || c.pubkey).join(', ');
|
||||
}
|
||||
|
||||
function renderChange(change: PropsForGroupUpdateType) {
|
||||
const ChangeItem = (change: PropsForGroupUpdateType): string => {
|
||||
const people = isTypeWithContact(change) ? getPeople(change) : [];
|
||||
|
||||
switch (change.type) {
|
||||
case 'name':
|
||||
return `${window.i18n('titleIsNow', [change.newName || ''])}`;
|
||||
return window.i18n('titleIsNow', change.newName || '');
|
||||
case 'add':
|
||||
if (!change.contacts || !change.contacts.length) {
|
||||
throw new Error('Group update add is missing contacts');
|
||||
}
|
||||
|
||||
const joinKey = change.contacts.length > 1 ? 'multipleJoinedTheGroup' : 'joinedTheGroup';
|
||||
|
||||
return <Intl id={joinKey} components={[people]} />;
|
||||
return window.i18n(joinKey, people);
|
||||
case 'remove':
|
||||
if (change.isMe) {
|
||||
return window.i18n('youLeftTheGroup');
|
||||
|
@ -63,8 +50,8 @@ function renderChange(change: PropsForGroupUpdateType) {
|
|||
}
|
||||
|
||||
const leftKey = change.contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
|
||||
return window.i18n(leftKey, people);
|
||||
|
||||
return <Intl id={leftKey} components={[people]} />;
|
||||
case 'kicked':
|
||||
if (change.isMe) {
|
||||
return window.i18n('youGotKickedFromGroup');
|
||||
|
@ -76,17 +63,20 @@ function renderChange(change: PropsForGroupUpdateType) {
|
|||
|
||||
const kickedKey =
|
||||
change.contacts.length > 1 ? 'multipleKickedFromTheGroup' : 'kickedFromTheGroup';
|
||||
return window.i18n(kickedKey, people);
|
||||
|
||||
return <Intl id={kickedKey} components={[people]} />;
|
||||
case 'general':
|
||||
return window.i18n('updatedTheGroup');
|
||||
default:
|
||||
window.log.error('Missing case error');
|
||||
throw new Error('Missing case error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GroupNotification = (props: PropsForGroupUpdate) => {
|
||||
const { changes, messageId, receivedAt, isUnread } = props;
|
||||
|
||||
const textChange = changes.map(ChangeItem)[0];
|
||||
|
||||
return (
|
||||
<ReadableMessage
|
||||
messageId={messageId}
|
||||
|
@ -94,13 +84,7 @@ export const GroupNotification = (props: PropsForGroupUpdate) => {
|
|||
isUnread={isUnread}
|
||||
key={`readable-message-${messageId}`}
|
||||
>
|
||||
<div className="module-group-notification" id={`msg-${props.messageId}`}>
|
||||
{(changes || []).map((change, index) => (
|
||||
<div key={index} className="module-group-notification__change">
|
||||
{renderChange(change)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<NotificationBubble notificationText={textChange} iconType="users" />
|
||||
</ReadableMessage>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ export const AudioPlayerWithEncryptedFile = (props: {
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [playbackSpeed, setPlaybackSpeed] = useState(1.0);
|
||||
const { urlToLoad } = useEncryptedFileFetch(props.src, props.contentType);
|
||||
const { urlToLoad } = useEncryptedFileFetch(props.src, props.contentType, false);
|
||||
const player = useRef<H5AudioPlayer | null>(null);
|
||||
|
||||
const autoPlaySetting = useSelector(getAudioAutoplay);
|
||||
|
@ -104,10 +104,10 @@ export const AudioPlayerWithEncryptedFile = (props: {
|
|||
]}
|
||||
customIcons={{
|
||||
play: (
|
||||
<SessionIcon iconType="play" iconSize={'small'} iconColor={'var(--color-text-subtle)'} />
|
||||
<SessionIcon iconType="play" iconSize="small" iconColor={'var(--color-text-subtle)'} />
|
||||
),
|
||||
pause: (
|
||||
<SessionIcon iconType="pause" iconSize={'small'} iconColor={'var(--color-text-subtle)'} />
|
||||
<SessionIcon iconType="pause" iconSize="small" iconColor={'var(--color-text-subtle)'} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|||
import { Spinner } from '../basic/Spinner';
|
||||
import { AttachmentType, AttachmentTypeWithPath } from '../../types/Attachment';
|
||||
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
|
||||
import { useDisableDrag } from '../../hooks/useDisableDrag';
|
||||
|
||||
type Props = {
|
||||
alt: string;
|
||||
|
@ -48,17 +49,13 @@ export const Image = (props: Props) => {
|
|||
width,
|
||||
} = props;
|
||||
|
||||
const onDragStart = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const onErrorUrlFilterering = useCallback(() => {
|
||||
if (url && onError) {
|
||||
onError();
|
||||
}
|
||||
return;
|
||||
}, [url, onError]);
|
||||
const disableDrag = useDisableDrag();
|
||||
|
||||
const { caption } = attachment || { caption: null };
|
||||
let { pending } = attachment || { pending: true };
|
||||
|
@ -68,7 +65,7 @@ export const Image = (props: Props) => {
|
|||
}
|
||||
const canClick = onClick && !pending;
|
||||
const role = canClick ? 'button' : undefined;
|
||||
const { loading, urlToLoad } = useEncryptedFileFetch(url || '', attachment.contentType);
|
||||
const { loading, urlToLoad } = useEncryptedFileFetch(url || '', attachment.contentType, false);
|
||||
// data will be url if loading is finished and '' if not
|
||||
const srcData = !loading ? urlToLoad : '';
|
||||
|
||||
|
@ -118,7 +115,7 @@ export const Image = (props: Props) => {
|
|||
height: forceSquare ? `${height}px` : '',
|
||||
}}
|
||||
src={srcData}
|
||||
onDragStart={onDragStart}
|
||||
onDragStart={disableDrag}
|
||||
/>
|
||||
)}
|
||||
{caption ? (
|
||||
|
@ -126,7 +123,7 @@ export const Image = (props: Props) => {
|
|||
className="module-image__caption-icon"
|
||||
src="images/caption-shadow.svg"
|
||||
alt={window.i18n('imageCaptionIconAlt')}
|
||||
onDragStart={onDragStart}
|
||||
onDragStart={disableDrag}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from '../../state/selectors/conversations';
|
||||
import { deleteMessagesById } from '../../interactions/conversations/unsendingInteractions';
|
||||
|
||||
const AvatarItem = (props: { pubkey: string | undefined }) => {
|
||||
const AvatarItem = (props: { pubkey: string }) => {
|
||||
const { pubkey } = props;
|
||||
|
||||
return <Avatar size={AvatarSize.S} pubkey={pubkey} />;
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { PubKey } from '../../session/types';
|
||||
|
||||
import { PropsForMissedCallNotification } from '../../state/ducks/conversations';
|
||||
import { getSelectedConversation } from '../../state/selectors/conversations';
|
||||
import { ReadableMessage } from './ReadableMessage';
|
||||
|
||||
export const StyledFakeMessageBubble = styled.div`
|
||||
background: var(--color-fake-chat-bubble-background);
|
||||
color: var(--color-text);
|
||||
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
margin: 10px auto;
|
||||
padding: 5px 0px;
|
||||
border-radius: 4px;
|
||||
word-break: break-word;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const MissedCallNotification = (props: PropsForMissedCallNotification) => {
|
||||
const { messageId, receivedAt, isUnread } = props;
|
||||
|
||||
const selectedConvoProps = useSelector(getSelectedConversation);
|
||||
|
||||
const displayName =
|
||||
selectedConvoProps?.profileName ||
|
||||
selectedConvoProps?.name ||
|
||||
(selectedConvoProps?.id && PubKey.shorten(selectedConvoProps?.id));
|
||||
|
||||
return (
|
||||
<ReadableMessage
|
||||
messageId={messageId}
|
||||
receivedAt={receivedAt}
|
||||
isUnread={isUnread}
|
||||
key={`readable-message-${messageId}`}
|
||||
>
|
||||
<StyledFakeMessageBubble>{window.i18n('callMissed', displayName)}</StyledFakeMessageBubble>
|
||||
</ReadableMessage>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,4 @@
|
|||
// tslint:disable:react-this-binding-issue
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import * as MIME from '../../../ts/types/MIME';
|
||||
|
@ -18,6 +16,7 @@ import {
|
|||
isPublicGroupConversation,
|
||||
} from '../../state/selectors/conversations';
|
||||
import { noop } from 'underscore';
|
||||
import { useDisableDrag } from '../../hooks/useDisableDrag';
|
||||
|
||||
export type QuotePropsWithoutListener = {
|
||||
attachment?: QuotedAttachmentType;
|
||||
|
@ -116,15 +115,11 @@ export const QuoteImage = (props: {
|
|||
icon?: string;
|
||||
}) => {
|
||||
const { url, icon, contentType, handleImageErrorBound } = props;
|
||||
const disableDrag = useDisableDrag();
|
||||
|
||||
const { loading, urlToLoad } = useEncryptedFileFetch(url, contentType);
|
||||
const { loading, urlToLoad } = useEncryptedFileFetch(url, contentType, false);
|
||||
const srcData = !loading ? urlToLoad : '';
|
||||
|
||||
const onDragStart = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const iconElement = icon ? (
|
||||
<div className="module-quote__icon-container__inner">
|
||||
<div className="module-quote__icon-container__circle-background">
|
||||
|
@ -143,7 +138,7 @@ export const QuoteImage = (props: {
|
|||
<img
|
||||
src={srcData}
|
||||
alt={window.i18n('quoteThumbnailAlt')}
|
||||
onDragStart={onDragStart}
|
||||
onDragStart={disableDrag}
|
||||
onError={handleImageErrorBound}
|
||||
/>
|
||||
{iconElement}
|
||||
|
|
|
@ -131,6 +131,7 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
|
|||
onChange={haveDoneFirstScroll && isAppFocused ? onVisible : noop}
|
||||
triggerOnce={false}
|
||||
trackVisibility={true}
|
||||
key={`inview-msg-${messageId}`}
|
||||
>
|
||||
{props.children}
|
||||
</InView>
|
||||
|
|
|
@ -1,41 +1,39 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Intl } from '../Intl';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { SessionIcon } from '../session/icon';
|
||||
import { PropsForExpirationTimer } from '../../state/ducks/conversations';
|
||||
import { ReadableMessage } from './ReadableMessage';
|
||||
import { NotificationBubble } from './notification-bubble/NotificationBubble';
|
||||
|
||||
const TimerNotificationContent = (props: PropsForExpirationTimer) => {
|
||||
const { pubkey, profileName, timespan, type, disabled } = props;
|
||||
const changeKey = disabled ? 'disabledDisappearingMessages' : 'theyChangedTheTimer';
|
||||
export const TimerNotification = (props: PropsForExpirationTimer) => {
|
||||
const { messageId, receivedAt, isUnread, pubkey, profileName, timespan, type, disabled } = props;
|
||||
|
||||
const contact = (
|
||||
<span key={`external-${pubkey}`} className="module-timer-notification__contact">
|
||||
{profileName || pubkey}
|
||||
</span>
|
||||
);
|
||||
const contact = profileName || pubkey;
|
||||
|
||||
let textToRender: string | undefined;
|
||||
switch (type) {
|
||||
case 'fromOther':
|
||||
return <Intl id={changeKey} components={[contact, timespan]} />;
|
||||
textToRender = disabled
|
||||
? window.i18n('disabledDisappearingMessages', [contact, timespan])
|
||||
: window.i18n('theyChangedTheTimer', [contact, timespan]);
|
||||
break;
|
||||
case 'fromMe':
|
||||
return disabled
|
||||
textToRender = disabled
|
||||
? window.i18n('youDisabledDisappearingMessages')
|
||||
: window.i18n('youChangedTheTimer', [timespan]);
|
||||
break;
|
||||
case 'fromSync':
|
||||
return disabled
|
||||
textToRender = disabled
|
||||
? window.i18n('disappearingMessagesDisabled')
|
||||
: window.i18n('timerSetOnSync', [timespan]);
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
};
|
||||
|
||||
export const TimerNotification = (props: PropsForExpirationTimer) => {
|
||||
const { messageId, receivedAt, isUnread } = props;
|
||||
|
||||
if (!textToRender || textToRender.length === 0) {
|
||||
throw new Error('textToRender invalid key used TimerNotification');
|
||||
}
|
||||
return (
|
||||
<ReadableMessage
|
||||
messageId={messageId}
|
||||
|
@ -43,17 +41,11 @@ export const TimerNotification = (props: PropsForExpirationTimer) => {
|
|||
isUnread={isUnread}
|
||||
key={`readable-message-${messageId}`}
|
||||
>
|
||||
<div className="module-timer-notification" id={`msg-${props.messageId}`}>
|
||||
<div className="module-timer-notification__message">
|
||||
<div>
|
||||
<SessionIcon iconType="stopwatch" iconSize={'small'} iconColor={'#ABABAB'} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TimerNotificationContent {...props} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NotificationBubble
|
||||
iconType="stopwatch"
|
||||
iconColor="inherit"
|
||||
notificationText={textToRender}
|
||||
/>
|
||||
</ReadableMessage>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { isImageTypeSupported, isVideoTypeSupported } from '../../../util/GoogleChrome';
|
||||
|
@ -6,6 +6,7 @@ import { MediaItemType } from '../../LightboxGallery';
|
|||
import { useEncryptedFileFetch } from '../../../hooks/useEncryptedFileFetch';
|
||||
import { showLightBox } from '../../../state/ducks/conversations';
|
||||
import { LightBoxOptions } from '../../session/conversation/SessionConversation';
|
||||
import { useDisableDrag } from '../../../hooks/useDisableDrag';
|
||||
|
||||
type Props = {
|
||||
mediaItem: MediaItemType;
|
||||
|
@ -20,14 +21,11 @@ const MediaGridItemContent = (props: Props) => {
|
|||
const urlToDecrypt = mediaItem.thumbnailObjectUrl || '';
|
||||
const [imageBroken, setImageBroken] = useState(false);
|
||||
|
||||
const { loading, urlToLoad } = useEncryptedFileFetch(urlToDecrypt, contentType);
|
||||
const { loading, urlToLoad } = useEncryptedFileFetch(urlToDecrypt, contentType, false);
|
||||
|
||||
const onDragStart = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, []);
|
||||
// data will be url if loading is finished and '' if not
|
||||
const srcData = !loading ? urlToLoad : '';
|
||||
const disableDrag = useDisableDrag();
|
||||
|
||||
const onImageError = () => {
|
||||
// tslint:disable-next-line no-console
|
||||
|
@ -57,7 +55,7 @@ const MediaGridItemContent = (props: Props) => {
|
|||
className="module-media-grid-item__image"
|
||||
src={srcData}
|
||||
onError={onImageError}
|
||||
onDragStart={onDragStart}
|
||||
onDragStart={disableDrag}
|
||||
/>
|
||||
);
|
||||
} else if (contentType && isVideoTypeSupported(contentType)) {
|
||||
|
@ -79,7 +77,7 @@ const MediaGridItemContent = (props: Props) => {
|
|||
className="module-media-grid-item__image"
|
||||
src={srcData}
|
||||
onError={onImageError}
|
||||
onDragStart={onDragStart}
|
||||
onDragStart={disableDrag}
|
||||
/>
|
||||
<div className="module-media-grid-item__circle-overlay">
|
||||
<div className="module-media-grid-item__play-overlay" />
|
||||
|
|
|
@ -110,7 +110,7 @@ export const ClickToTrustSender = (props: { messageId: string }) => {
|
|||
|
||||
return (
|
||||
<StyledTrustSenderUI onClick={openConfirmationModal}>
|
||||
<SessionIcon iconSize={'small'} iconType="gallery" />
|
||||
<SessionIcon iconSize="small" iconType="gallery" />
|
||||
<ClickToDownload>{window.i18n('clickToTrustContact')}</ClickToDownload>
|
||||
</StyledTrustSenderUI>
|
||||
);
|
||||
|
|
|
@ -45,8 +45,22 @@ export const MessageContentWithStatuses = (props: Props) => {
|
|||
[window.contextMenuShown, props?.messageId, multiSelectMode, props?.isDetailView]
|
||||
);
|
||||
|
||||
const onDoubleClickReplyToMessage = () => {
|
||||
void replyToMessage(messageId);
|
||||
const onDoubleClickReplyToMessage = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const currentSelection = window.getSelection();
|
||||
const currentSelectionString = currentSelection?.toString() || undefined;
|
||||
|
||||
// if multiple word are selected, consider that this double click was actually NOT used to reply to
|
||||
// but to select
|
||||
if (
|
||||
!currentSelectionString ||
|
||||
currentSelectionString.length === 0 ||
|
||||
!currentSelectionString.includes(' ')
|
||||
) {
|
||||
void replyToMessage(messageId);
|
||||
currentSelection?.empty();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const { messageId, onQuoteClick, ctxMenuID, isDetailView } = props;
|
||||
|
@ -61,7 +75,7 @@ export const MessageContentWithStatuses = (props: Props) => {
|
|||
className={classNames('module-message', `module-message--${direction}`)}
|
||||
role="button"
|
||||
onClick={onClickOnMessageOuterContainer}
|
||||
onDoubleClick={onDoubleClickReplyToMessage}
|
||||
onDoubleClickCapture={onDoubleClickReplyToMessage}
|
||||
style={{ width: hasAttachments && isTrustedForAttachmentDownload ? 'min-content' : 'auto' }}
|
||||
>
|
||||
<MessageStatus messageId={messageId} isCorrectSide={isIncoming} />
|
||||
|
|
|
@ -63,7 +63,7 @@ export const MessagePreview = (props: Props) => {
|
|||
<div className="module-message__link-preview__icon_container">
|
||||
<div className="module-message__link-preview__icon_container__inner">
|
||||
<div className="module-message__link-preview__icon-container__circle-background">
|
||||
<SessionIcon iconType="link" iconSize={'small'} />
|
||||
<SessionIcon iconType="link" iconSize="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@ const MessageStatusSending = () => {
|
|||
const iconColor = 'var(--color-text)';
|
||||
return (
|
||||
<MessageStatusSendingContainer>
|
||||
<SessionIcon rotateDuration={2} iconColor={iconColor} iconType="sending" iconSize={'tiny'} />
|
||||
<SessionIcon rotateDuration={2} iconColor={iconColor} iconType="sending" iconSize="tiny" />
|
||||
</MessageStatusSendingContainer>
|
||||
);
|
||||
};
|
||||
|
@ -26,7 +26,7 @@ const MessageStatusSent = () => {
|
|||
|
||||
return (
|
||||
<MessageStatusSendingContainer>
|
||||
<SessionIcon iconColor={iconColor} iconType="circleCheck" iconSize={'tiny'} />
|
||||
<SessionIcon iconColor={iconColor} iconType="circleCheck" iconSize="tiny" />
|
||||
</MessageStatusSendingContainer>
|
||||
);
|
||||
};
|
||||
|
@ -36,7 +36,7 @@ const MessageStatusRead = () => {
|
|||
|
||||
return (
|
||||
<MessageStatusSendingContainer>
|
||||
<SessionIcon iconColor={iconColor} iconType="doubleCheckCircleFilled" iconSize={'tiny'} />
|
||||
<SessionIcon iconColor={iconColor} iconType="doubleCheckCircleFilled" iconSize="tiny" />
|
||||
</MessageStatusSendingContainer>
|
||||
);
|
||||
};
|
||||
|
@ -48,7 +48,7 @@ const MessageStatusError = () => {
|
|||
|
||||
return (
|
||||
<MessageStatusSendingContainer onClick={showDebugLog} title={window.i18n('sendFailed')}>
|
||||
<SessionIcon iconColor={'var(--color-destructive'} iconType="error" iconSize={'tiny'} />
|
||||
<SessionIcon iconColor={'var(--color-destructive'} iconType="error" iconSize="tiny" />
|
||||
</MessageStatusSendingContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { PubKey } from '../../../session/types';
|
||||
|
||||
import { CallNotificationType, PropsForCallNotification } from '../../../state/ducks/conversations';
|
||||
import { getSelectedConversation } from '../../../state/selectors/conversations';
|
||||
import { SessionIconType } from '../../session/icon';
|
||||
import { ReadableMessage } from '../ReadableMessage';
|
||||
import { NotificationBubble } from './NotificationBubble';
|
||||
|
||||
type StyleType = Record<
|
||||
CallNotificationType,
|
||||
{ notificationTextKey: string; iconType: SessionIconType; iconColor: string }
|
||||
>;
|
||||
|
||||
const style: StyleType = {
|
||||
'missed-call': {
|
||||
notificationTextKey: 'callMissed',
|
||||
iconType: 'callMissed',
|
||||
iconColor: 'var(--color-destructive)',
|
||||
},
|
||||
'started-call': {
|
||||
notificationTextKey: 'startedACall',
|
||||
iconType: 'callOutgoing',
|
||||
iconColor: 'inherit',
|
||||
},
|
||||
'answered-a-call': {
|
||||
notificationTextKey: 'answeredACall',
|
||||
iconType: 'callIncoming',
|
||||
iconColor: 'inherit',
|
||||
},
|
||||
};
|
||||
|
||||
export const CallNotification = (props: PropsForCallNotification) => {
|
||||
const { messageId, receivedAt, isUnread, notificationType } = props;
|
||||
|
||||
const selectedConvoProps = useSelector(getSelectedConversation);
|
||||
|
||||
const displayName =
|
||||
selectedConvoProps?.profileName ||
|
||||
selectedConvoProps?.name ||
|
||||
(selectedConvoProps?.id && PubKey.shorten(selectedConvoProps?.id));
|
||||
|
||||
const styleItem = style[notificationType];
|
||||
const notificationText = window.i18n(styleItem.notificationTextKey, displayName);
|
||||
if (!window.i18n(styleItem.notificationTextKey)) {
|
||||
throw new Error(`invalid i18n key ${styleItem.notificationTextKey}`);
|
||||
}
|
||||
const iconType = styleItem.iconType;
|
||||
const iconColor = styleItem.iconColor;
|
||||
|
||||
return (
|
||||
<ReadableMessage
|
||||
messageId={messageId}
|
||||
receivedAt={receivedAt}
|
||||
isUnread={isUnread}
|
||||
key={`readable-message-${messageId}`}
|
||||
>
|
||||
<NotificationBubble
|
||||
notificationText={notificationText}
|
||||
iconType={iconType}
|
||||
iconColor={iconColor}
|
||||
/>
|
||||
</ReadableMessage>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { SessionIcon, SessionIconType } from '../../session/icon';
|
||||
|
||||
const NotificationBubbleFlex = styled.div`
|
||||
display: flex;
|
||||
background: var(--color-fake-chat-bubble-background);
|
||||
color: var(--color-text);
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
margin: 10px auto;
|
||||
padding: 5px 10px;
|
||||
border-radius: 16px;
|
||||
word-break: break-word;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const NotificationBubbleText = styled.div`
|
||||
color: inherit;
|
||||
margin: auto auto;
|
||||
`;
|
||||
|
||||
const NotificationBubbleIconContainer = styled.div`
|
||||
margin: auto 10px;
|
||||
width: 15px;
|
||||
height: 25px;
|
||||
`;
|
||||
|
||||
export const NotificationBubble = (props: {
|
||||
notificationText: string;
|
||||
iconType?: SessionIconType;
|
||||
iconColor?: string;
|
||||
}) => {
|
||||
const { notificationText, iconType, iconColor } = props;
|
||||
return (
|
||||
<NotificationBubbleFlex>
|
||||
{iconType && (
|
||||
<NotificationBubbleIconContainer>
|
||||
<SessionIcon
|
||||
iconSize="small"
|
||||
iconType={iconType}
|
||||
iconColor={iconColor}
|
||||
iconPadding="auto 10px"
|
||||
/>
|
||||
</NotificationBubbleIconContainer>
|
||||
)}
|
||||
<NotificationBubbleText>{notificationText}</NotificationBubbleText>
|
||||
{iconType && <NotificationBubbleIconContainer />}
|
||||
</NotificationBubbleFlex>
|
||||
);
|
||||
};
|
|
@ -79,7 +79,7 @@ export class SessionModal extends React.PureComponent<Props, State> {
|
|||
<div className={classNames('session-modal__header', headerReverse && 'reverse')}>
|
||||
<div className="session-modal__header__close">
|
||||
{showExitIcon ? (
|
||||
<SessionIconButton iconType="exit" iconSize={'small'} onClick={this.close} />
|
||||
<SessionIconButton iconType="exit" iconSize="small" onClick={this.close} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="session-modal__header__title">{title}</div>
|
||||
|
|
|
@ -56,7 +56,7 @@ export const LeftPaneSectionHeader = (props: Props) => {
|
|||
{label && <Tab label={label} type={0} isSelected={true} key={label} />}
|
||||
{buttonIcon && (
|
||||
<SessionButton onClick={buttonClicked} key="compose">
|
||||
<SessionIcon iconType={buttonIcon} iconSize={'small'} iconColor="white" />
|
||||
<SessionIcon iconType={buttonIcon} iconSize="small" iconColor="white" />
|
||||
</SessionButton>
|
||||
)}
|
||||
</div>
|
||||
|
@ -80,6 +80,7 @@ const BannerInner = () => {
|
|||
buttonType={SessionButtonType.Default}
|
||||
text={window.i18n('recoveryPhraseRevealButtonText')}
|
||||
onClick={showRecoveryPhraseModal}
|
||||
dataTestId="reveal-recovery-phrase"
|
||||
/>
|
||||
</StyledBannerInner>
|
||||
);
|
||||
|
|
|
@ -28,15 +28,16 @@ type Props = {
|
|||
buttonColor: SessionButtonColor;
|
||||
onClick: any;
|
||||
children?: ReactNode;
|
||||
dataTestId?: string;
|
||||
};
|
||||
|
||||
export const SessionButton = (props: Props) => {
|
||||
const { buttonType, buttonColor, text, disabled } = props;
|
||||
const { buttonType, dataTestId, buttonColor, text, disabled, onClick } = props;
|
||||
|
||||
const clickHandler = (e: any) => {
|
||||
if (props.onClick) {
|
||||
if (onClick) {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -53,6 +54,7 @@ export const SessionButton = (props: Props) => {
|
|||
className={classNames('session-button', ...buttonTypes, buttonColor, disabled && 'disabled')}
|
||||
role="button"
|
||||
onClick={onClickFn}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{props.children || text}
|
||||
</div>
|
||||
|
|
|
@ -156,7 +156,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
|
|||
return (
|
||||
<div className="module-left-pane-overlay">
|
||||
<div className="exit">
|
||||
<SessionIconButton iconSize={'small'} iconType="exit" onClick={onCloseClick} />
|
||||
<SessionIconButton iconSize="small" iconType="exit" onClick={onCloseClick} />
|
||||
</div>
|
||||
|
||||
<SpacerMD />
|
||||
|
|
|
@ -34,7 +34,7 @@ export const SessionDropdown = (props: Props) => {
|
|||
role="button"
|
||||
>
|
||||
{label}
|
||||
<SessionIcon iconType="chevron" iconSize={'small'} iconRotation={chevronOrientation} />
|
||||
<SessionIcon iconType="chevron" iconSize="small" iconRotation={chevronOrientation} />
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
|
|
|
@ -36,7 +36,7 @@ export const SessionDropdownItem = (props: Props) => {
|
|||
role="button"
|
||||
onClick={clickHandler}
|
||||
>
|
||||
{icon ? <SessionIcon iconType={icon} iconSize={'small'} /> : ''}
|
||||
{icon ? <SessionIcon iconType={icon} iconSize="small" /> : ''}
|
||||
<div className="item-content">{content}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { SessionIconButton } from './icon';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
label?: string;
|
||||
error?: string;
|
||||
type?: string;
|
||||
|
@ -15,117 +15,100 @@ interface Props {
|
|||
onEnterPressed?: any;
|
||||
autoFocus?: boolean;
|
||||
ref?: any;
|
||||
}
|
||||
inputDataTestId?: string;
|
||||
};
|
||||
|
||||
interface State {
|
||||
inputValue: string;
|
||||
forceShow: boolean;
|
||||
}
|
||||
const LabelItem = (props: { inputValue: string; label?: string }) => {
|
||||
return (
|
||||
<label
|
||||
htmlFor="session-input-floating-label"
|
||||
className={classNames(
|
||||
props.inputValue !== ''
|
||||
? 'session-input-with-label-container filled'
|
||||
: 'session-input-with-label-container'
|
||||
)}
|
||||
>
|
||||
{props.label}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export class SessionInput extends React.PureComponent<Props, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const ErrorItem = (props: { error: string | undefined }) => {
|
||||
return (
|
||||
<label
|
||||
htmlFor="session-input-floating-label"
|
||||
className={classNames('session-input-with-label-container filled error')}
|
||||
>
|
||||
{props.error}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
this.updateInputValue = this.updateInputValue.bind(this);
|
||||
this.renderShowHideButton = this.renderShowHideButton.bind(this);
|
||||
const ShowHideButton = (props: { toggleForceShow: () => void }) => {
|
||||
return <SessionIconButton iconType="eye" iconSize="medium" onClick={props.toggleForceShow} />;
|
||||
};
|
||||
|
||||
this.state = {
|
||||
inputValue: '',
|
||||
forceShow: false,
|
||||
};
|
||||
}
|
||||
export const SessionInput = (props: Props) => {
|
||||
const {
|
||||
autoFocus,
|
||||
placeholder,
|
||||
type,
|
||||
value,
|
||||
maxLength,
|
||||
enableShowHide,
|
||||
error,
|
||||
label,
|
||||
onValueChanged,
|
||||
inputDataTestId,
|
||||
} = props;
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [forceShow, setForceShow] = useState(false);
|
||||
|
||||
public render() {
|
||||
const { autoFocus, placeholder, type, value, maxLength, enableShowHide, error } = this.props;
|
||||
const { forceShow } = this.state;
|
||||
const correctType = forceShow ? 'text' : type;
|
||||
|
||||
const correctType = forceShow ? 'text' : type;
|
||||
const updateInputValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const val = e.target.value;
|
||||
setInputValue(val);
|
||||
if (onValueChanged) {
|
||||
onValueChanged(val);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="session-input-with-label-container">
|
||||
{error ? this.renderError() : this.renderLabel()}
|
||||
<input
|
||||
id="session-input-floating-label"
|
||||
type={correctType}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
maxLength={maxLength}
|
||||
autoFocus={autoFocus}
|
||||
onChange={e => {
|
||||
this.updateInputValue(e);
|
||||
}}
|
||||
className={classNames(enableShowHide ? 'session-input-floating-label-show-hide' : '')}
|
||||
// just incase onChange isn't triggered
|
||||
onBlur={e => {
|
||||
this.updateInputValue(e);
|
||||
}}
|
||||
onKeyPress={event => {
|
||||
if (event.key === 'Enter' && this.props.onEnterPressed) {
|
||||
this.props.onEnterPressed();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{enableShowHide && this.renderShowHideButton()}
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLabel() {
|
||||
const { inputValue } = this.state;
|
||||
const { label } = this.props;
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor="session-input-floating-label"
|
||||
className={classNames(
|
||||
inputValue !== ''
|
||||
? 'session-input-with-label-container filled'
|
||||
: 'session-input-with-label-container'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
private renderError() {
|
||||
const { error } = this.props;
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor="session-input-floating-label"
|
||||
className={classNames('session-input-with-label-container filled error')}
|
||||
>
|
||||
{error}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
private renderShowHideButton() {
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconType="eye"
|
||||
iconSize="medium"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
forceShow: !this.state.forceShow,
|
||||
});
|
||||
return (
|
||||
<div className="session-input-with-label-container">
|
||||
{error ? (
|
||||
<ErrorItem error={props.error} />
|
||||
) : (
|
||||
<LabelItem inputValue={inputValue} label={label} />
|
||||
)}
|
||||
<input
|
||||
id="session-input-floating-label"
|
||||
type={correctType}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
maxLength={maxLength}
|
||||
autoFocus={autoFocus}
|
||||
data-testid={inputDataTestId}
|
||||
onChange={updateInputValue}
|
||||
className={classNames(enableShowHide ? 'session-input-floating-label-show-hide' : '')}
|
||||
// just incase onChange isn't triggered
|
||||
onBlur={updateInputValue}
|
||||
onKeyPress={event => {
|
||||
if (event.key === 'Enter' && props.onEnterPressed) {
|
||||
props.onEnterPressed();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private updateInputValue(e: any) {
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
inputValue: e.target.value,
|
||||
});
|
||||
|
||||
if (this.props.onValueChanged) {
|
||||
this.props.onValueChanged(e.target.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
{enableShowHide && (
|
||||
<ShowHideButton
|
||||
toggleForceShow={() => {
|
||||
setForceShow(!forceShow);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -74,6 +74,7 @@ const SessionJoinableRoomAvatar = (props: JoinableRoomProps) => {
|
|||
size={AvatarSize.XS}
|
||||
base64Data={props.base64Data}
|
||||
{...props}
|
||||
pubkey=""
|
||||
onAvatarClick={() => props.onClick(props.completeUrl)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -27,7 +27,7 @@ type Props = {
|
|||
onUnselect?: (selectedMember: ContactType) => void;
|
||||
};
|
||||
|
||||
const AvatarItem = (props: { memberPubkey?: string }) => {
|
||||
const AvatarItem = (props: { memberPubkey: string }) => {
|
||||
return <Avatar size={AvatarSize.XS} pubkey={props.memberPubkey} />;
|
||||
};
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => {
|
|||
<div className={classNames('session-modal__header', headerReverse && 'reverse')}>
|
||||
<div className="session-modal__header__close">
|
||||
{showExitIcon ? (
|
||||
<SessionIconButton iconType="exit" iconSize={'small'} onClick={props.onClose} />
|
||||
<SessionIconButton iconType="exit" iconSize="small" onClick={props.onClose} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="session-modal__header__title">{title}</div>
|
||||
|
|
|
@ -135,7 +135,7 @@ export const DraggableCallContainer = () => {
|
|||
autoPlay={true}
|
||||
isVideoMuted={remoteStreamVideoIsMuted}
|
||||
/>
|
||||
{remoteStreamVideoIsMuted && (
|
||||
{remoteStreamVideoIsMuted && ongoingCallPubkey && (
|
||||
<CenteredAvatarInDraggable>
|
||||
<Avatar size={AvatarSize.XL} pubkey={ongoingCallPubkey} />
|
||||
</CenteredAvatarInDraggable>
|
||||
|
|
|
@ -158,7 +158,7 @@ export const InConversationCallContainer = () => {
|
|||
videoRefRemote.current.muted = true;
|
||||
}
|
||||
|
||||
if (!ongoingCallWithFocused) {
|
||||
if (!ongoingCallWithFocused || !ongoingCallPubkey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import _ from 'underscore';
|
|||
import { useConversationUsername } from '../../../hooks/useParamSelector';
|
||||
import { ed25519Str } from '../../../session/onions/onionPath';
|
||||
import { CallManager } from '../../../session/utils';
|
||||
import { callTimeoutMs } from '../../../session/utils/calling/CallManager';
|
||||
import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/call';
|
||||
import { Avatar, AvatarSize } from '../../Avatar';
|
||||
import { SessionButton, SessionButtonColor } from '../SessionButton';
|
||||
|
@ -24,12 +25,10 @@ export const CallWindow = styled.div`
|
|||
border: var(--session-border);
|
||||
`;
|
||||
|
||||
const IncomingCallAvatatContainer = styled.div`
|
||||
const IncomingCallAvatarContainer = styled.div`
|
||||
padding: 0 0 2rem 0;
|
||||
`;
|
||||
|
||||
const timeoutMs = 60000;
|
||||
|
||||
export const IncomingCallDialog = () => {
|
||||
const hasIncomingCall = useSelector(getHasIncomingCall);
|
||||
const incomingCallFromPubkey = useSelector(getHasIncomingCallFrom);
|
||||
|
@ -42,11 +41,11 @@ export const IncomingCallDialog = () => {
|
|||
window.log.info(
|
||||
`call missed with ${ed25519Str(
|
||||
incomingCallFromPubkey
|
||||
)} as dialog was not interacted with for ${timeoutMs} ms`
|
||||
)} as the dialog was not interacted with for ${callTimeoutMs} ms`
|
||||
);
|
||||
await CallManager.USER_rejectIncomingCallRequest(incomingCallFromPubkey);
|
||||
}
|
||||
}, timeoutMs);
|
||||
}, callTimeoutMs);
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -70,16 +69,16 @@ export const IncomingCallDialog = () => {
|
|||
}
|
||||
};
|
||||
const from = useConversationUsername(incomingCallFromPubkey);
|
||||
if (!hasIncomingCall) {
|
||||
if (!hasIncomingCall || !incomingCallFromPubkey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasIncomingCall) {
|
||||
return (
|
||||
<SessionWrapperModal title={window.i18n('incomingCallFrom', from)}>
|
||||
<IncomingCallAvatatContainer>
|
||||
<IncomingCallAvatarContainer>
|
||||
<Avatar size={AvatarSize.XL} pubkey={incomingCallFromPubkey} />
|
||||
</IncomingCallAvatatContainer>
|
||||
</IncomingCallAvatarContainer>
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton
|
||||
text={window.i18n('decline')}
|
||||
|
|
|
@ -4,18 +4,18 @@ import { useSelector } from 'react-redux';
|
|||
import useKey from 'react-use/lib/useKey';
|
||||
import { PropsForDataExtractionNotification, QuoteClickOptions } from '../../../models/messageType';
|
||||
import {
|
||||
PropsForCallNotification,
|
||||
PropsForExpirationTimer,
|
||||
PropsForGroupInvitation,
|
||||
PropsForGroupUpdate,
|
||||
PropsForMissedCallNotification,
|
||||
} from '../../../state/ducks/conversations';
|
||||
import { getSortedMessagesTypesOfSelectedConversation } from '../../../state/selectors/conversations';
|
||||
import { CallNotification } from '../../conversation/notification-bubble/CallNotification';
|
||||
import { DataExtractionNotification } from '../../conversation/DataExtractionNotification';
|
||||
import { GroupInvitation } from '../../conversation/GroupInvitation';
|
||||
import { GroupNotification } from '../../conversation/GroupNotification';
|
||||
import { Message } from '../../conversation/Message';
|
||||
import { MessageDateBreak } from '../../conversation/message/DateBreak';
|
||||
import { MissedCallNotification } from '../../conversation/MissedCallNotification';
|
||||
import { TimerNotification } from '../../conversation/TimerNotification';
|
||||
import { SessionLastSeenIndicator } from './SessionLastSeenIndicator';
|
||||
|
||||
|
@ -86,14 +86,10 @@ export const SessionMessagesList = (props: {
|
|||
return [<TimerNotification key={messageId} {...msgProps} />, dateBreak, unreadIndicator];
|
||||
}
|
||||
|
||||
if (messageProps.message?.messageType === 'missed-call-notification') {
|
||||
const msgProps = messageProps.message.props as PropsForMissedCallNotification;
|
||||
if (messageProps.message?.messageType === 'call-notification') {
|
||||
const msgProps = messageProps.message.props as PropsForCallNotification;
|
||||
|
||||
return [
|
||||
<MissedCallNotification key={messageId} {...msgProps} />,
|
||||
dateBreak,
|
||||
unreadIndicator,
|
||||
];
|
||||
return [<CallNotification key={messageId} {...msgProps} />, dateBreak, unreadIndicator];
|
||||
}
|
||||
|
||||
if (!messageProps) {
|
||||
|
|
|
@ -75,7 +75,7 @@ export const SessionQuotedMessageComposition = () => {
|
|||
margin={'var(--margins-xs)'}
|
||||
>
|
||||
<ReplyingTo>{window.i18n('replyingToMessage')}</ReplyingTo>
|
||||
<SessionIconButton iconType="exit" iconSize={'small'} onClick={removeQuotedMessage} />
|
||||
<SessionIconButton iconType="exit" iconSize="small" onClick={removeQuotedMessage} />
|
||||
</Flex>
|
||||
<QuotedMessageCompositionReply>
|
||||
<Flex container={true} justifyContent="space-between" margin={'var(--margins-xs)'}>
|
||||
|
|
|
@ -3,6 +3,9 @@ export type SessionIconType =
|
|||
| 'arrow'
|
||||
| 'bell'
|
||||
| 'brand'
|
||||
| 'callIncoming'
|
||||
| 'callMissed'
|
||||
| 'callOutgoing'
|
||||
| 'caret'
|
||||
| 'chatBubble'
|
||||
| 'check'
|
||||
|
@ -95,6 +98,24 @@ export const icons = {
|
|||
viewBox: '0 0 404.085 448.407',
|
||||
ratio: 1,
|
||||
},
|
||||
callIncoming: {
|
||||
path:
|
||||
'M14.414 7l3.293-3.293a1 1 0 00-1.414-1.414L13 5.586V4a1 1 0 10-2 0v4.003a.996.996 0 00.617.921A.997.997 0 0012 9h4a1 1 0 100-2h-1.586zM2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z',
|
||||
viewBox: '0 0 20 20',
|
||||
ratio: 1,
|
||||
},
|
||||
callOutgoing: {
|
||||
path:
|
||||
'M17.924 2.617a.997.997 0 00-.215-.322l-.004-.004A.997.997 0 0017 2h-4a1 1 0 100 2h1.586l-3.293 3.293a1 1 0 001.414 1.414L16 5.414V7a1 1 0 102 0V3a.997.997 0 00-.076-.383zM2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z',
|
||||
viewBox: '0 0 20 20',
|
||||
ratio: 1,
|
||||
},
|
||||
callMissed: {
|
||||
path:
|
||||
'M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3zM16.707 3.293a1 1 0 010 1.414L15.414 6l1.293 1.293a1 1 0 01-1.414 1.414L14 7.414l-1.293 1.293a1 1 0 11-1.414-1.414L12.586 6l-1.293-1.293a1 1 0 011.414-1.414L14 4.586l1.293-1.293a1 1 0 011.414 0z',
|
||||
viewBox: '0 0 20 20',
|
||||
ratio: 1,
|
||||
},
|
||||
caret: {
|
||||
path: 'M127.5 191.25L255 63.75L0 63.75L127.5 191.25Z',
|
||||
viewBox: '-200 -200 640 640',
|
||||
|
|
|
@ -20,6 +20,7 @@ const DisplayNameInput = (props: {
|
|||
maxLength={MAX_USERNAME_LENGTH}
|
||||
onValueChanged={props.onDisplayNameChanged}
|
||||
onEnterPressed={props.handlePressEnter}
|
||||
inputDataTestId="display-name-input"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -41,6 +42,7 @@ const RecoveryPhraseInput = (props: {
|
|||
enableShowHide={true}
|
||||
onValueChanged={props.onSeedChanged}
|
||||
onEnterPressed={props.handlePressEnter}
|
||||
inputDataTestId="recovery-phrase-input"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -39,6 +39,7 @@ const RestoreUsingRecoveryPhraseButton = (props: { onRecoveryButtonClicked: () =
|
|||
buttonType={SessionButtonType.BrandOutline}
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
text={window.i18n('restoreUsingRecoveryPhrase')}
|
||||
dataTestId="restore-using-recovery"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
// tslint:disable-next-line: no-submodule-imports
|
||||
import useUpdate from 'react-use/lib/useUpdate';
|
||||
import { CallManager } from '../../../../session/utils';
|
||||
import { sessionPassword, updateConfirmModal } from '../../../../state/ducks/modalDialog';
|
||||
import { toggleMessageRequests } from '../../../../state/ducks/userConfig';
|
||||
import { getIsMessageRequestsEnabled } from '../../../../state/selectors/userConfig';
|
||||
|
@ -23,6 +24,7 @@ const toggleCallMediaPermissions = async (triggerUIUpdate: () => void) => {
|
|||
onClickOk: async () => {
|
||||
await window.toggleCallMediaPermissionsTo(true);
|
||||
triggerUIUpdate();
|
||||
CallManager.onTurnedOnCallMediaPermissions();
|
||||
},
|
||||
onClickCancel: async () => {
|
||||
await window.toggleCallMediaPermissionsTo(false);
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* This memoized function just returns a callback which can be used to disable the onDragStart event
|
||||
*/
|
||||
export const useDisableDrag = () => {
|
||||
const cb = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
return cb;
|
||||
};
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from '../session/crypto/DecryptedAttachmentsManager';
|
||||
import { perfEnd, perfStart } from '../session/utils/Performance';
|
||||
|
||||
export const useEncryptedFileFetch = (url: string, contentType: string) => {
|
||||
export const useEncryptedFileFetch = (url: string, contentType: string, isAvatar: boolean) => {
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
const [urlToLoad, setUrlToLoad] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
@ -16,7 +16,7 @@ export const useEncryptedFileFetch = (url: string, contentType: string) => {
|
|||
async function fetchUrl() {
|
||||
perfStart(`getDecryptedMediaUrl-${url}`);
|
||||
|
||||
const decryptedUrl = await getDecryptedMediaUrl(url, contentType);
|
||||
const decryptedUrl = await getDecryptedMediaUrl(url, contentType, isAvatar);
|
||||
perfEnd(`getDecryptedMediaUrl-${url}`, `getDecryptedMediaUrl-${url}`);
|
||||
|
||||
if (mountedRef.current) {
|
||||
|
|
|
@ -39,3 +39,16 @@ export function useOurConversationUsername() {
|
|||
export function useIsMe(pubkey?: string) {
|
||||
return pubkey && pubkey === UserUtils.getOurPubKeyStrFromCache();
|
||||
}
|
||||
|
||||
export function useIsClosedGroup(convoId?: string) {
|
||||
return useSelector((state: StateType) => {
|
||||
if (!convoId) {
|
||||
return false;
|
||||
}
|
||||
const convo = state.conversations.conversationLookup[convoId];
|
||||
if (!convo) {
|
||||
return false;
|
||||
}
|
||||
return (convo.isGroup && !convo.isPublic) || false;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -331,7 +331,7 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) {
|
|||
return;
|
||||
}
|
||||
|
||||
const decryptedAvatarUrl = await getDecryptedMediaUrl(currentAttachmentPath, IMAGE_JPEG);
|
||||
const decryptedAvatarUrl = await getDecryptedMediaUrl(currentAttachmentPath, IMAGE_JPEG, true);
|
||||
|
||||
if (!decryptedAvatarUrl) {
|
||||
window.log.warn('Could not decrypt avatar stored locally..');
|
||||
|
|
|
@ -50,6 +50,7 @@ import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsMana
|
|||
import { IMAGE_JPEG } from '../types/MIME';
|
||||
import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils';
|
||||
import { getLatestTimestampOffset } from '../session/snode_api/SNodeAPI';
|
||||
import { createLastMessageUpdate } from '../types/Conversation';
|
||||
|
||||
export enum ConversationTypeEnum {
|
||||
GROUP = 'group',
|
||||
|
@ -89,7 +90,7 @@ export interface ConversationAttributes {
|
|||
sessionRestoreSeen?: boolean;
|
||||
is_medium_group?: boolean;
|
||||
type: string;
|
||||
avatarPointer?: any;
|
||||
avatarPointer?: string;
|
||||
avatar?: any;
|
||||
/* Avatar hash is currently used for opengroupv2. it's sha256 hash of the base64 avatar data. */
|
||||
avatarHash?: string;
|
||||
|
@ -131,7 +132,7 @@ export interface ConversationAttributesOptionals {
|
|||
sessionRestoreSeen?: boolean;
|
||||
is_medium_group?: boolean;
|
||||
type: string;
|
||||
avatarPointer?: any;
|
||||
avatarPointer?: string;
|
||||
avatar?: any;
|
||||
avatarHash?: string;
|
||||
server?: any;
|
||||
|
@ -900,9 +901,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
const lastMessageJSON = lastMessageModel ? lastMessageModel.toJSON() : null;
|
||||
const lastMessageStatusModel = lastMessageModel
|
||||
? lastMessageModel.getMessagePropStatus()
|
||||
: null;
|
||||
const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate({
|
||||
currentTimestamp: this.get('active_at') || null,
|
||||
: undefined;
|
||||
const lastMessageUpdate = createLastMessageUpdate({
|
||||
currentTimestamp: this.get('active_at'),
|
||||
lastMessage: lastMessageJSON,
|
||||
lastMessageStatus: lastMessageStatusModel,
|
||||
lastMessageNotificationText: lastMessageModel ? lastMessageModel.getNotificationText() : null,
|
||||
|
@ -1057,6 +1058,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
);
|
||||
const unreadCount = await this.getUnreadCount();
|
||||
this.set({ unreadCount });
|
||||
this.updateLastMessage();
|
||||
|
||||
await this.commit();
|
||||
return model;
|
||||
}
|
||||
|
@ -1487,7 +1490,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
const avatarUrl = this.getAvatarPath();
|
||||
const noIconUrl = 'images/session/session_icon_32.png';
|
||||
if (avatarUrl) {
|
||||
const decryptedAvatarUrl = await getDecryptedMediaUrl(avatarUrl, IMAGE_JPEG);
|
||||
const decryptedAvatarUrl = await getDecryptedMediaUrl(avatarUrl, IMAGE_JPEG, true);
|
||||
|
||||
if (!decryptedAvatarUrl) {
|
||||
window.log.warn('Could not decrypt avatar stored locally for getNotificationIcon..');
|
||||
|
|
|
@ -88,7 +88,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
const propsForGroupInvitation = this.getPropsForGroupInvitation();
|
||||
const propsForGroupNotification = this.getPropsForGroupNotification();
|
||||
const propsForTimerNotification = this.getPropsForTimerNotification();
|
||||
const isMissedCall = this.get('isMissedCall');
|
||||
const callNotificationType = this.get('callNotificationType');
|
||||
const messageProps: MessageModelPropsWithoutConvoProps = {
|
||||
propsForMessage: this.getPropsForMessage(),
|
||||
};
|
||||
|
@ -105,9 +105,9 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
messageProps.propsForTimerNotification = propsForTimerNotification;
|
||||
}
|
||||
|
||||
if (isMissedCall) {
|
||||
messageProps.propsForMissedCall = {
|
||||
isMissedCall,
|
||||
if (callNotificationType) {
|
||||
messageProps.propsForCallNotification = {
|
||||
notificationType: callNotificationType,
|
||||
messageId: this.id,
|
||||
receivedAt: this.get('received_at') || Date.now(),
|
||||
isUnread: this.isUnread(),
|
||||
|
@ -239,6 +239,21 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
getConversationController().getContactProfileNameOrShortenedPubKey(dataExtraction.source)
|
||||
);
|
||||
}
|
||||
if (this.get('callNotificationType')) {
|
||||
const displayName = getConversationController().getContactProfileNameOrShortenedPubKey(
|
||||
this.get('conversationId')
|
||||
);
|
||||
const callNotificationType = this.get('callNotificationType');
|
||||
if (callNotificationType === 'missed-call') {
|
||||
return window.i18n('callMissed', displayName);
|
||||
}
|
||||
if (callNotificationType === 'started-call') {
|
||||
return window.i18n('startedACall', displayName);
|
||||
}
|
||||
if (callNotificationType === 'answered-a-call') {
|
||||
return window.i18n('answeredACall', displayName);
|
||||
}
|
||||
}
|
||||
return this.get('body');
|
||||
}
|
||||
|
||||
|
@ -498,7 +513,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (this.isDataExtractionNotification()) {
|
||||
if (this.isDataExtractionNotification() || this.get('callNotificationType')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import _ from 'underscore';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { PropsForMessageWithConvoProps } from '../state/ducks/conversations';
|
||||
import { CallNotificationType, PropsForMessageWithConvoProps } from '../state/ducks/conversations';
|
||||
import { AttachmentTypeWithPath } from '../types/Attachment';
|
||||
|
||||
export type MessageModelType = 'incoming' | 'outgoing';
|
||||
|
@ -109,7 +109,7 @@ export interface MessageAttributes {
|
|||
*/
|
||||
isDeleted?: boolean;
|
||||
|
||||
isMissedCall?: boolean;
|
||||
callNotificationType?: CallNotificationType;
|
||||
}
|
||||
|
||||
export interface DataExtractionNotificationMsg {
|
||||
|
@ -179,7 +179,7 @@ export interface MessageAttributesOptionals {
|
|||
direction?: any;
|
||||
messageHash?: string;
|
||||
isDeleted?: boolean;
|
||||
isMissedCall?: boolean;
|
||||
callNotificationType?: CallNotificationType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -74,19 +74,19 @@ export async function handleCallMessage(
|
|||
if (type === SignalService.CallMessage.Type.ANSWER) {
|
||||
await removeFromCache(envelope);
|
||||
|
||||
await CallManager.handleCallTypeAnswer(sender, callMessage);
|
||||
await CallManager.handleCallTypeAnswer(sender, callMessage, sentTimestamp);
|
||||
|
||||
return;
|
||||
}
|
||||
if (type === SignalService.CallMessage.Type.ICE_CANDIDATES) {
|
||||
await removeFromCache(envelope);
|
||||
|
||||
await CallManager.handleCallTypeIceCandidates(sender, callMessage);
|
||||
await CallManager.handleCallTypeIceCandidates(sender, callMessage, sentTimestamp);
|
||||
|
||||
return;
|
||||
}
|
||||
await removeFromCache(envelope);
|
||||
|
||||
// if this another type of call message, just add it to the manager
|
||||
await CallManager.handleOtherCallTypes(sender, callMessage);
|
||||
await CallManager.handleOtherCallTypes(sender, callMessage, sentTimestamp);
|
||||
}
|
||||
|
|
|
@ -11,20 +11,29 @@ import * as fse from 'fs-extra';
|
|||
import { decryptAttachmentBuffer } from '../../types/Attachment';
|
||||
import { DURATION } from '../constants';
|
||||
|
||||
// FIXME.
|
||||
// add a way to remove the blob when the attachment file path is removed (message removed?)
|
||||
// do not hardcode the password
|
||||
const urlToDecryptedBlobMap = new Map<string, { decrypted: string; lastAccessTimestamp: number }>();
|
||||
const urlToDecryptedBlobMap = new Map<
|
||||
string,
|
||||
{ decrypted: string; lastAccessTimestamp: number; forceRetain: boolean }
|
||||
>();
|
||||
const urlToDecryptingPromise = new Map<string, Promise<string>>();
|
||||
|
||||
export const cleanUpOldDecryptedMedias = () => {
|
||||
const currentTimestamp = Date.now();
|
||||
let countCleaned = 0;
|
||||
let countKept = 0;
|
||||
let keptAsAvatars = 0;
|
||||
|
||||
window?.log?.info('Starting cleaning of medias blobs...');
|
||||
for (const iterator of urlToDecryptedBlobMap) {
|
||||
// if the last access is older than one hour, revoke the url and remove it.
|
||||
if (iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.HOURS * 1) {
|
||||
if (
|
||||
iterator[1].forceRetain &&
|
||||
iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.DAYS * 7
|
||||
) {
|
||||
// keep forceRetained items for at most 7 days
|
||||
keptAsAvatars++;
|
||||
} else if (iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.HOURS * 1) {
|
||||
// if the last access is older than one hour, revoke the url and remove it.
|
||||
|
||||
URL.revokeObjectURL(iterator[1].decrypted);
|
||||
urlToDecryptedBlobMap.delete(iterator[0]);
|
||||
countCleaned++;
|
||||
|
@ -32,10 +41,16 @@ export const cleanUpOldDecryptedMedias = () => {
|
|||
countKept++;
|
||||
}
|
||||
}
|
||||
window?.log?.info(`Clean medias blobs: cleaned/kept: ${countCleaned}:${countKept}`);
|
||||
window?.log?.info(
|
||||
`Clean medias blobs: cleaned/kept/keptAsAvatars: ${countCleaned}:${countKept}:${keptAsAvatars}`
|
||||
);
|
||||
};
|
||||
|
||||
export const getDecryptedMediaUrl = async (url: string, contentType: string): Promise<string> => {
|
||||
export const getDecryptedMediaUrl = async (
|
||||
url: string,
|
||||
contentType: string,
|
||||
isAvatar: boolean
|
||||
): Promise<string> => {
|
||||
if (!url) {
|
||||
return url;
|
||||
}
|
||||
|
@ -50,11 +65,13 @@ export const getDecryptedMediaUrl = async (url: string, contentType: string): Pr
|
|||
// if it's not, the hook caller has to fallback to setting the img src as an url to the file instead and load it
|
||||
if (urlToDecryptedBlobMap.has(url)) {
|
||||
// refresh the last access timestamp so we keep the one being currently in use
|
||||
const existingObjUrl = urlToDecryptedBlobMap.get(url)?.decrypted as string;
|
||||
const existing = urlToDecryptedBlobMap.get(url);
|
||||
const existingObjUrl = existing?.decrypted as string;
|
||||
|
||||
urlToDecryptedBlobMap.set(url, {
|
||||
decrypted: existingObjUrl,
|
||||
lastAccessTimestamp: Date.now(),
|
||||
forceRetain: existing?.forceRetain || false,
|
||||
});
|
||||
// typescript does not realize that the has above makes sure the get is not undefined
|
||||
|
||||
|
@ -80,6 +97,7 @@ export const getDecryptedMediaUrl = async (url: string, contentType: string): Pr
|
|||
urlToDecryptedBlobMap.set(url, {
|
||||
decrypted: obj,
|
||||
lastAccessTimestamp: Date.now(),
|
||||
forceRetain: isAvatar,
|
||||
});
|
||||
}
|
||||
urlToDecryptingPromise.delete(url);
|
||||
|
|
|
@ -20,6 +20,10 @@ function startRinging() {
|
|||
void ringingAudio.play();
|
||||
}
|
||||
|
||||
export function getIsRinging() {
|
||||
return currentlyRinging;
|
||||
}
|
||||
|
||||
export function setIsRinging(isRinging: boolean) {
|
||||
if (!currentlyRinging && isRinging) {
|
||||
startRinging();
|
||||
|
|
|
@ -5,6 +5,7 @@ import { KeyPair } from '../../../libtextsecure/libsignal-protocol';
|
|||
import { PubKey } from '../types';
|
||||
import { fromHexToArray, toHex } from './String';
|
||||
import { getConversationController } from '../conversations';
|
||||
import { LokiProfile } from '../../types/Message';
|
||||
|
||||
export type HexKeyPair = {
|
||||
pubKey: string;
|
||||
|
@ -93,13 +94,7 @@ export function setSignWithRecoveryPhrase(isLinking: boolean) {
|
|||
window.textsecure.storage.user.setSignWithRecoveryPhrase(isLinking);
|
||||
}
|
||||
|
||||
export interface OurLokiProfile {
|
||||
displayName: string;
|
||||
avatarPointer: string;
|
||||
profileKey: Uint8Array | null;
|
||||
}
|
||||
|
||||
export function getOurProfile(): OurLokiProfile | undefined {
|
||||
export function getOurProfile(): LokiProfile | undefined {
|
||||
try {
|
||||
// Secondary devices have their profile stored
|
||||
// in their primary device's conversation
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import { MessageUtils, ToastUtils, UserUtils } from '../';
|
||||
import { getCallMediaPermissionsSettings } from '../../../components/session/settings/SessionSettings';
|
||||
import { getConversationById } from '../../../data/data';
|
||||
import { MessageModelType } from '../../../models/messageType';
|
||||
import { SignalService } from '../../../protobuf';
|
||||
import { openConversationWithMessages } from '../../../state/ducks/conversations';
|
||||
|
@ -21,15 +20,18 @@ import { PubKey } from '../../types';
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { PnServer } from '../../../pushnotification';
|
||||
import { setIsRinging } from '../RingingManager';
|
||||
import { getIsRinging, setIsRinging } from '../RingingManager';
|
||||
import { getBlackSilenceMediaStream } from './Silence';
|
||||
import { getMessageQueue } from '../..';
|
||||
import { MessageSender } from '../../sending';
|
||||
import { DURATION } from '../../constants';
|
||||
|
||||
// tslint:disable: function-name
|
||||
|
||||
export type InputItem = { deviceId: string; label: string };
|
||||
|
||||
export const callTimeoutMs = 30000;
|
||||
|
||||
/**
|
||||
* This uuid is set only once we accepted a call or started one.
|
||||
*/
|
||||
|
@ -88,10 +90,19 @@ export function removeVideoEventsListener(uniqueId: string) {
|
|||
callVideoListeners();
|
||||
}
|
||||
|
||||
type CachedCallMessageType = {
|
||||
type: SignalService.CallMessage.Type;
|
||||
sdps: Array<string>;
|
||||
sdpMLineIndexes: Array<number>;
|
||||
sdpMids: Array<string>;
|
||||
uuid: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per call cache.
|
||||
*/
|
||||
const callCache = new Map<string, Map<string, Array<SignalService.CallMessage>>>();
|
||||
const callCache = new Map<string, Map<string, Array<CachedCallMessageType>>>();
|
||||
|
||||
let peerConnection: RTCPeerConnection | null;
|
||||
let dataChannel: RTCDataChannel | null;
|
||||
|
@ -293,6 +304,8 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
|
|||
if (sender?.track) {
|
||||
sender.track.enabled = false;
|
||||
}
|
||||
const silence = getBlackSilenceMediaStream().getAudioTracks()[0];
|
||||
sender?.replaceTrack(silence);
|
||||
// do the same changes locally
|
||||
localStream?.getAudioTracks().forEach(t => {
|
||||
t.stop();
|
||||
|
@ -322,10 +335,14 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
|
|||
return s.track?.kind === audioTrack.kind;
|
||||
});
|
||||
window.log.info('replacing audio track');
|
||||
|
||||
// we actually do not need to toggle the track here, as toggling it here unmuted here locally (so we start to hear ourselves)
|
||||
// do the same changes locally
|
||||
localStream?.getAudioTracks().forEach(t => {
|
||||
t.stop();
|
||||
localStream?.removeTrack(t);
|
||||
});
|
||||
if (audioSender) {
|
||||
await audioSender.replaceTrack(audioTrack);
|
||||
// we actually do not need to toggle the track here, as toggling it here unmuted here locally (so we start to hear ourselves)
|
||||
} else {
|
||||
throw new Error('Failed to get sender for selectAudioInputByDeviceId ');
|
||||
}
|
||||
|
@ -439,22 +456,38 @@ export async function USER_callRecipient(recipient: string) {
|
|||
return;
|
||||
}
|
||||
await updateConnectedDevices();
|
||||
const now = Date.now();
|
||||
window?.log?.info(`starting call with ${ed25519Str(recipient)}..`);
|
||||
window.inboxStore?.dispatch(startingCallWith({ pubkey: recipient }));
|
||||
window.inboxStore?.dispatch(
|
||||
startingCallWith({
|
||||
pubkey: recipient,
|
||||
})
|
||||
);
|
||||
if (peerConnection) {
|
||||
throw new Error('USER_callRecipient peerConnection is already initialized ');
|
||||
}
|
||||
currentCallUUID = uuidv4();
|
||||
const justCreatedCallUUID = currentCallUUID;
|
||||
peerConnection = createOrGetPeerConnection(recipient);
|
||||
// send a pre offer just to wake up the device on the remote side
|
||||
const preOfferMsg = new CallMessage({
|
||||
timestamp: Date.now(),
|
||||
timestamp: now,
|
||||
type: SignalService.CallMessage.Type.PRE_OFFER,
|
||||
uuid: currentCallUUID,
|
||||
});
|
||||
|
||||
window.log.info('Sending preOffer message to ', ed25519Str(recipient));
|
||||
|
||||
const calledConvo = getConversationController().get(recipient);
|
||||
await calledConvo?.addSingleMessage({
|
||||
conversationId: calledConvo.id,
|
||||
source: UserUtils.getOurPubKeyStrFromCache(),
|
||||
type: 'outgoing',
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
expireTimer: 0,
|
||||
callNotificationType: 'started-call',
|
||||
unread: 0,
|
||||
});
|
||||
// we do it manually as the sendToPubkeyNonDurably rely on having a message saved to the db for MessageSentSuccess
|
||||
// which is not the case for a pre offer message (the message only exists in memory)
|
||||
const rawMessage = await MessageUtils.toRawMessage(PubKey.cast(recipient), preOfferMsg);
|
||||
|
@ -464,6 +497,17 @@ export async function USER_callRecipient(recipient: string) {
|
|||
await openMediaDevicesAndAddTracks();
|
||||
setIsRinging(true);
|
||||
await createOfferAndSendIt(recipient);
|
||||
|
||||
// close and end the call if callTimeoutMs is reached ans still not connected
|
||||
global.setTimeout(async () => {
|
||||
if (justCreatedCallUUID === currentCallUUID && getIsRinging()) {
|
||||
window.log.info(
|
||||
'calling timeout reached. hanging up the call we started:',
|
||||
justCreatedCallUUID
|
||||
);
|
||||
await USER_hangup(recipient);
|
||||
}
|
||||
}, callTimeoutMs);
|
||||
}
|
||||
|
||||
const iceCandidates: Array<RTCIceCandidate> = new Array();
|
||||
|
@ -762,6 +806,18 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
|
|||
await peerConnection.addIceCandidate(candicate);
|
||||
}
|
||||
}
|
||||
const now = Date.now();
|
||||
const callerConvo = getConversationController().get(fromSender);
|
||||
await callerConvo?.addSingleMessage({
|
||||
conversationId: callerConvo.id,
|
||||
source: UserUtils.getOurPubKeyStrFromCache(),
|
||||
type: 'incoming',
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
expireTimer: 0,
|
||||
callNotificationType: 'answered-a-call',
|
||||
unread: 0,
|
||||
});
|
||||
await buildAnswerAndSendIt(fromSender);
|
||||
}
|
||||
|
||||
|
@ -809,6 +865,7 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) {
|
|||
if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) {
|
||||
closeVideoCall();
|
||||
}
|
||||
await addMissedCallMessage(fromSender, Date.now());
|
||||
}
|
||||
|
||||
async function sendCallMessageAndSync(callmessage: CallMessage, user: string) {
|
||||
|
@ -907,6 +964,20 @@ export function isCallRejected(uuid: string) {
|
|||
return rejectedCallUUIDS.has(uuid);
|
||||
}
|
||||
|
||||
function getCachedMessageFromCallMessage(
|
||||
callMessage: SignalService.CallMessage,
|
||||
envelopeTimestamp: number
|
||||
) {
|
||||
return {
|
||||
type: callMessage.type,
|
||||
sdps: callMessage.sdps,
|
||||
sdpMLineIndexes: callMessage.sdpMLineIndexes,
|
||||
sdpMids: callMessage.sdpMids,
|
||||
uuid: callMessage.uuid,
|
||||
timestamp: envelopeTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleCallTypeOffer(
|
||||
sender: string,
|
||||
callMessage: SignalService.CallMessage,
|
||||
|
@ -920,6 +991,9 @@ export async function handleCallTypeOffer(
|
|||
window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID);
|
||||
|
||||
if (!getCallMediaPermissionsSettings()) {
|
||||
const cachedMsg = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp);
|
||||
pushCallMessageToCallCache(sender, remoteCallUUID, cachedMsg);
|
||||
|
||||
await handleMissedCall(sender, incomingOfferTimestamp, true);
|
||||
return;
|
||||
}
|
||||
|
@ -979,8 +1053,9 @@ export async function handleCallTypeOffer(
|
|||
}
|
||||
setIsRinging(true);
|
||||
}
|
||||
const cachedMessage = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp);
|
||||
|
||||
pushCallMessageToCallCache(sender, remoteCallUUID, callMessage);
|
||||
pushCallMessageToCallCache(sender, remoteCallUUID, cachedMessage);
|
||||
} catch (err) {
|
||||
window.log?.error(`Error handling offer message ${err}`);
|
||||
}
|
||||
|
@ -991,7 +1066,7 @@ export async function handleMissedCall(
|
|||
incomingOfferTimestamp: number,
|
||||
isBecauseOfCallPermission: boolean
|
||||
) {
|
||||
const incomingCallConversation = await getConversationById(sender);
|
||||
const incomingCallConversation = getConversationController().get(sender);
|
||||
setIsRinging(false);
|
||||
if (!isBecauseOfCallPermission) {
|
||||
ToastUtils.pushedMissedCall(
|
||||
|
@ -1007,26 +1082,30 @@ export async function handleMissedCall(
|
|||
);
|
||||
}
|
||||
|
||||
await addMissedCallMessage(sender, incomingOfferTimestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
async function addMissedCallMessage(callerPubkey: string, sentAt: number) {
|
||||
const incomingCallConversation = getConversationController().get(callerPubkey);
|
||||
|
||||
await incomingCallConversation?.addSingleMessage({
|
||||
conversationId: incomingCallConversation.id,
|
||||
source: sender,
|
||||
conversationId: callerPubkey,
|
||||
source: callerPubkey,
|
||||
type: 'incoming' as MessageModelType,
|
||||
sent_at: incomingOfferTimestamp,
|
||||
sent_at: sentAt,
|
||||
received_at: Date.now(),
|
||||
expireTimer: 0,
|
||||
isMissedCall: true,
|
||||
callNotificationType: 'missed-call',
|
||||
unread: 1,
|
||||
});
|
||||
incomingCallConversation?.updateLastMessage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function getOwnerOfCallUUID(callUUID: string) {
|
||||
for (const deviceKey of callCache.keys()) {
|
||||
for (const callUUIDEntry of callCache.get(deviceKey) as Map<
|
||||
string,
|
||||
Array<SignalService.CallMessage>
|
||||
Array<CachedCallMessageType>
|
||||
>) {
|
||||
if (callUUIDEntry[0] === callUUID) {
|
||||
return deviceKey;
|
||||
|
@ -1036,7 +1115,11 @@ function getOwnerOfCallUUID(callUUID: string) {
|
|||
return null;
|
||||
}
|
||||
|
||||
export async function handleCallTypeAnswer(sender: string, callMessage: SignalService.CallMessage) {
|
||||
export async function handleCallTypeAnswer(
|
||||
sender: string,
|
||||
callMessage: SignalService.CallMessage,
|
||||
envelopeTimestamp: number
|
||||
) {
|
||||
if (!callMessage.sdps || callMessage.sdps.length === 0) {
|
||||
window.log.warn('cannot handle answered message without signal description proto sdps');
|
||||
return;
|
||||
|
@ -1083,8 +1166,9 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe
|
|||
} else {
|
||||
window.log.info(`handling callMessage ANSWER from ${callMessageUUID}`);
|
||||
}
|
||||
const cachedMessage = getCachedMessageFromCallMessage(callMessage, envelopeTimestamp);
|
||||
|
||||
pushCallMessageToCallCache(sender, callMessageUUID, callMessage);
|
||||
pushCallMessageToCallCache(sender, callMessageUUID, cachedMessage);
|
||||
|
||||
if (!peerConnection) {
|
||||
window.log.info('handleCallTypeAnswer without peer connection. Dropping');
|
||||
|
@ -1114,7 +1198,8 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe
|
|||
|
||||
export async function handleCallTypeIceCandidates(
|
||||
sender: string,
|
||||
callMessage: SignalService.CallMessage
|
||||
callMessage: SignalService.CallMessage,
|
||||
envelopeTimestamp: number
|
||||
) {
|
||||
if (!callMessage.sdps || callMessage.sdps.length === 0) {
|
||||
window.log.warn('cannot handle iceCandicates message without candidates');
|
||||
|
@ -1126,8 +1211,9 @@ export async function handleCallTypeIceCandidates(
|
|||
return;
|
||||
}
|
||||
window.log.info('handling callMessage ICE_CANDIDATES');
|
||||
const cachedMessage = getCachedMessageFromCallMessage(callMessage, envelopeTimestamp);
|
||||
|
||||
pushCallMessageToCallCache(sender, remoteCallUUID, callMessage);
|
||||
pushCallMessageToCallCache(sender, remoteCallUUID, cachedMessage);
|
||||
if (currentCallUUID && callMessage.uuid === currentCallUUID) {
|
||||
await addIceCandidateToExistingPeerConnection(callMessage);
|
||||
}
|
||||
|
@ -1155,13 +1241,18 @@ async function addIceCandidateToExistingPeerConnection(callMessage: SignalServic
|
|||
}
|
||||
|
||||
// tslint:disable-next-line: no-async-without-await
|
||||
export async function handleOtherCallTypes(sender: string, callMessage: SignalService.CallMessage) {
|
||||
export async function handleOtherCallTypes(
|
||||
sender: string,
|
||||
callMessage: SignalService.CallMessage,
|
||||
envelopeTimestamp: number
|
||||
) {
|
||||
const remoteCallUUID = callMessage.uuid;
|
||||
if (!remoteCallUUID || remoteCallUUID.length === 0) {
|
||||
window.log.warn('handleOtherCallTypes has no valid uuid');
|
||||
return;
|
||||
}
|
||||
pushCallMessageToCallCache(sender, remoteCallUUID, callMessage);
|
||||
const cachedMessage = getCachedMessageFromCallMessage(callMessage, envelopeTimestamp);
|
||||
pushCallMessageToCallCache(sender, remoteCallUUID, cachedMessage);
|
||||
}
|
||||
|
||||
function clearCallCacheFromPubkeyAndUUID(sender: string, callUUID: string) {
|
||||
|
@ -1181,7 +1272,7 @@ function createCallCacheForPubkeyAndUUID(sender: string, uuid: string) {
|
|||
function pushCallMessageToCallCache(
|
||||
sender: string,
|
||||
uuid: string,
|
||||
callMessage: SignalService.CallMessage
|
||||
callMessage: CachedCallMessageType
|
||||
) {
|
||||
createCallCacheForPubkeyAndUUID(sender, uuid);
|
||||
callCache
|
||||
|
@ -1189,3 +1280,23 @@ function pushCallMessageToCallCache(
|
|||
?.get(uuid)
|
||||
?.push(callMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the settings of call media permissions is set to true from the settings page.
|
||||
* Check for any recent offer and display it to the user if needed.
|
||||
*/
|
||||
export function onTurnedOnCallMediaPermissions() {
|
||||
// this is not ideal as this might take the not latest sender from callCache
|
||||
callCache.forEach((sender, key) => {
|
||||
sender.forEach(msgs => {
|
||||
for (const msg of msgs.reverse()) {
|
||||
if (
|
||||
msg.type === SignalService.CallMessage.Type.OFFER &&
|
||||
Date.now() - msg.timestamp < DURATION.MINUTES * 1
|
||||
) {
|
||||
window.inboxStore?.dispatch(incomingCall({ pubkey: key }));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,8 +17,9 @@ import { QuotedAttachmentType } from '../../components/conversation/Quote';
|
|||
import { perfEnd, perfStart } from '../../session/utils/Performance';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
export type PropsForMissedCallNotification = {
|
||||
isMissedCall: boolean;
|
||||
export type CallNotificationType = 'missed-call' | 'started-call' | 'answered-a-call';
|
||||
export type PropsForCallNotification = {
|
||||
notificationType: CallNotificationType;
|
||||
messageId: string;
|
||||
receivedAt: number;
|
||||
isUnread: boolean;
|
||||
|
@ -30,7 +31,7 @@ export type MessageModelPropsWithoutConvoProps = {
|
|||
propsForTimerNotification?: PropsForExpirationTimer;
|
||||
propsForDataExtractionNotification?: PropsForDataExtractionNotification;
|
||||
propsForGroupNotification?: PropsForGroupUpdate;
|
||||
propsForMissedCall?: PropsForMissedCallNotification;
|
||||
propsForCallNotification?: PropsForCallNotification;
|
||||
};
|
||||
|
||||
export type MessageModelPropsWithConvoProps = SortedMessageModelProps & {
|
||||
|
|
|
@ -83,19 +83,6 @@ export const getSelectedConversationIsPublic = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
const getConversationId = (_whatever: any, id: string) => id;
|
||||
|
||||
export const getConversationById = createSelector(
|
||||
getConversations,
|
||||
getConversationId,
|
||||
(
|
||||
state: ConversationsStateType,
|
||||
convoId: string | undefined
|
||||
): ReduxConversationType | undefined => {
|
||||
return convoId ? state.conversationLookup[convoId] : undefined;
|
||||
}
|
||||
);
|
||||
|
||||
export const getIsTypingEnabled = createSelector(
|
||||
getConversations,
|
||||
getSelectedConversationKey,
|
||||
|
@ -190,7 +177,7 @@ export type MessagePropsType =
|
|||
| 'timer-notification'
|
||||
| 'regular-message'
|
||||
| 'unread-indicator'
|
||||
| 'missed-call-notification';
|
||||
| 'call-notification';
|
||||
|
||||
export const getSortedMessagesTypesOfSelectedConversation = createSelector(
|
||||
getSortedMessagesOfSelectedConversation,
|
||||
|
@ -257,14 +244,14 @@ export const getSortedMessagesTypesOfSelectedConversation = createSelector(
|
|||
};
|
||||
}
|
||||
|
||||
if (msg.propsForMissedCall) {
|
||||
if (msg.propsForCallNotification) {
|
||||
return {
|
||||
showUnreadIndicator: isFirstUnread,
|
||||
showDateBreak,
|
||||
message: {
|
||||
messageType: 'missed-call-notification',
|
||||
messageType: 'call-notification',
|
||||
props: {
|
||||
...msg.propsForMissedCall,
|
||||
...msg.propsForCallNotification,
|
||||
messageId: msg.propsForMessage.id,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { assert } from 'chai';
|
||||
import { LastMessageStatusType } from '../../state/ducks/conversations';
|
||||
|
||||
import * as Conversation from '../../types/Conversation';
|
||||
import { IncomingMessage } from '../../types/Message';
|
||||
|
@ -9,8 +10,8 @@ describe('Conversation', () => {
|
|||
const input = {};
|
||||
const expected = {
|
||||
lastMessage: '',
|
||||
lastMessageStatus: null,
|
||||
timestamp: null,
|
||||
lastMessageStatus: undefined,
|
||||
timestamp: undefined,
|
||||
};
|
||||
|
||||
const actual = Conversation.createLastMessageUpdate(input);
|
||||
|
@ -21,7 +22,7 @@ describe('Conversation', () => {
|
|||
it('should update last message text and timestamp', () => {
|
||||
const input = {
|
||||
currentTimestamp: 555,
|
||||
lastMessageStatus: 'read',
|
||||
lastMessageStatus: 'read' as LastMessageStatusType,
|
||||
lastMessage: {
|
||||
type: 'outgoing',
|
||||
conversationId: 'foo',
|
||||
|
@ -32,7 +33,7 @@ describe('Conversation', () => {
|
|||
};
|
||||
const expected = {
|
||||
lastMessage: 'New outgoing message',
|
||||
lastMessageStatus: 'read',
|
||||
lastMessageStatus: 'read' as LastMessageStatusType,
|
||||
timestamp: 666,
|
||||
};
|
||||
|
||||
|
@ -60,7 +61,7 @@ describe('Conversation', () => {
|
|||
};
|
||||
const expected = {
|
||||
lastMessage: 'Last message before expired',
|
||||
lastMessageStatus: null,
|
||||
lastMessageStatus: undefined,
|
||||
timestamp: 555,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { LastMessageStatusType } from '../state/ducks/conversations';
|
||||
import { Message } from './Message';
|
||||
|
||||
interface ConversationLastMessageUpdate {
|
||||
lastMessage: string;
|
||||
lastMessageStatus: string | null;
|
||||
timestamp: number | null;
|
||||
lastMessageStatus: LastMessageStatusType;
|
||||
timestamp: number | undefined;
|
||||
}
|
||||
|
||||
export const createLastMessageUpdate = ({
|
||||
|
@ -14,14 +15,14 @@ export const createLastMessageUpdate = ({
|
|||
}: {
|
||||
currentTimestamp?: number;
|
||||
lastMessage?: Message;
|
||||
lastMessageStatus?: string;
|
||||
lastMessageStatus?: LastMessageStatusType;
|
||||
lastMessageNotificationText?: string;
|
||||
}): ConversationLastMessageUpdate => {
|
||||
if (!lastMessage) {
|
||||
return {
|
||||
lastMessage: '',
|
||||
lastMessageStatus: null,
|
||||
timestamp: null,
|
||||
lastMessageStatus: undefined,
|
||||
timestamp: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -35,7 +36,7 @@ export const createLastMessageUpdate = ({
|
|||
|
||||
return {
|
||||
lastMessage: lastMessageNotificationText || '',
|
||||
lastMessageStatus: lastMessageStatus || null,
|
||||
timestamp: newTimestamp || null,
|
||||
lastMessageStatus: lastMessageStatus || undefined,
|
||||
timestamp: newTimestamp || undefined,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -51,6 +51,6 @@ type MessageSchemaVersion5 = Partial<
|
|||
|
||||
export type LokiProfile = {
|
||||
displayName: string;
|
||||
avatarPointer: string;
|
||||
avatarPointer?: string;
|
||||
profileKey: Uint8Array | null;
|
||||
};
|
||||
|
|
|
@ -151,7 +151,7 @@ export const saveAttachmentToDisk = async ({
|
|||
messageSender: string;
|
||||
conversationId: string;
|
||||
}) => {
|
||||
const decryptedUrl = await getDecryptedMediaUrl(attachment.url, attachment.contentType);
|
||||
const decryptedUrl = await getDecryptedMediaUrl(attachment.url, attachment.contentType, false);
|
||||
save({
|
||||
attachment: { ...attachment, url: decryptedUrl },
|
||||
document,
|
||||
|
|
Loading…
Reference in New Issue