Voice-note animation fixes
This commit is contained in:
parent
56f8842ed2
commit
458eb2ea81
|
@ -162,6 +162,7 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
|
||||||
if (!playing) {
|
if (!playing) {
|
||||||
audio.play();
|
audio.play();
|
||||||
setPlaying(true);
|
setPlaying(true);
|
||||||
|
setPlayed(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Number.isNaN(audio.duration)) {
|
if (!Number.isNaN(audio.duration)) {
|
||||||
|
@ -195,10 +196,6 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
|
||||||
? { playing, playbackRate, currentTime, duration: audio.duration }
|
? { playing, playbackRate, currentTime, duration: audio.duration }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const setPlayedAction = () => {
|
|
||||||
setPlayed(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageAudio
|
<MessageAudio
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -208,7 +205,6 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
|
||||||
active={active}
|
active={active}
|
||||||
played={_played}
|
played={_played}
|
||||||
loadAndPlayMessageAudio={loadAndPlayMessageAudio}
|
loadAndPlayMessageAudio={loadAndPlayMessageAudio}
|
||||||
onFirstPlayed={setPlayedAction}
|
|
||||||
setIsPlaying={setIsPlayingAction}
|
setIsPlaying={setIsPlayingAction}
|
||||||
setPlaybackRate={setPlaybackRateAction}
|
setPlaybackRate={setPlaybackRateAction}
|
||||||
setCurrentTime={setCurrentTimeAction}
|
setCurrentTime={setCurrentTimeAction}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import type {
|
||||||
} from '../../state/ducks/conversations';
|
} from '../../state/ducks/conversations';
|
||||||
import type { ViewStoryActionCreatorType } from '../../state/ducks/stories';
|
import type { ViewStoryActionCreatorType } from '../../state/ducks/stories';
|
||||||
import type { TimelineItemType } from './TimelineItem';
|
import type { TimelineItemType } from './TimelineItem';
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import type { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
import { Avatar, AvatarSize } from '../Avatar';
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
import { AvatarSpacer } from '../AvatarSpacer';
|
import { AvatarSpacer } from '../AvatarSpacer';
|
||||||
import { Spinner } from '../Spinner';
|
import { Spinner } from '../Spinner';
|
||||||
|
@ -61,6 +61,7 @@ import {
|
||||||
isImageAttachment,
|
isImageAttachment,
|
||||||
isVideo,
|
isVideo,
|
||||||
isGIF,
|
isGIF,
|
||||||
|
isPlayed,
|
||||||
} from '../../types/Attachment';
|
} from '../../types/Attachment';
|
||||||
import type { EmbeddedContactType } from '../../types/EmbeddedContact';
|
import type { EmbeddedContactType } from '../../types/EmbeddedContact';
|
||||||
|
|
||||||
|
@ -179,7 +180,6 @@ export type AudioAttachmentProps = {
|
||||||
|
|
||||||
kickOffAttachmentDownload(): void;
|
kickOffAttachmentDownload(): void;
|
||||||
onCorrupted(): void;
|
onCorrupted(): void;
|
||||||
onFirstPlayed(): void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum GiftBadgeStates {
|
export enum GiftBadgeStates {
|
||||||
|
@ -902,7 +902,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
isSticker,
|
isSticker,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
markViewed,
|
|
||||||
quote,
|
quote,
|
||||||
readStatus,
|
readStatus,
|
||||||
reducedMotion,
|
reducedMotion,
|
||||||
|
@ -1017,19 +1016,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isAudio(attachments)) {
|
if (isAudio(attachments)) {
|
||||||
let played: boolean;
|
const played = isPlayed(direction, status, readStatus);
|
||||||
switch (direction) {
|
|
||||||
case 'outgoing':
|
|
||||||
played = status === 'viewed';
|
|
||||||
break;
|
|
||||||
case 'incoming':
|
|
||||||
played = readStatus === ReadStatus.Viewed;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
log.error(missingCaseError(direction));
|
|
||||||
played = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderAudioAttachment({
|
return renderAudioAttachment({
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -1064,9 +1051,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
messageId: id,
|
messageId: id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onFirstPlayed() {
|
|
||||||
markViewed(id);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2021-2022 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useRef, useEffect, useState } from 'react';
|
import React, { useCallback, useRef, useEffect, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
import { animated, useSpring } from '@react-spring/web';
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
@ -38,7 +38,6 @@ export type OwnProps = Readonly<{
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
kickOffAttachmentDownload(): void;
|
kickOffAttachmentDownload(): void;
|
||||||
onCorrupted(): void;
|
onCorrupted(): void;
|
||||||
onFirstPlayed(): void;
|
|
||||||
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
|
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -63,6 +62,7 @@ type ButtonProps = {
|
||||||
mod?: string;
|
mod?: string;
|
||||||
label: string;
|
label: string;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
animateClick?: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onMouseDown?: () => void;
|
onMouseDown?: () => void;
|
||||||
onMouseUp?: () => void;
|
onMouseUp?: () => void;
|
||||||
|
@ -91,7 +91,7 @@ const BIG_INCREMENT = 5;
|
||||||
|
|
||||||
const PLAYBACK_RATES = [1, 1.5, 2, 0.5];
|
const PLAYBACK_RATES = [1, 1.5, 2, 0.5];
|
||||||
|
|
||||||
const SPRING_DEFAULTS = {
|
const SPRING_CONFIG = {
|
||||||
mass: 0.5,
|
mass: 0.5,
|
||||||
tension: 350,
|
tension: 350,
|
||||||
friction: 20,
|
friction: 20,
|
||||||
|
@ -131,33 +131,42 @@ const Button: React.FC<ButtonProps> = props => {
|
||||||
children,
|
children,
|
||||||
onClick,
|
onClick,
|
||||||
visible = true,
|
visible = true,
|
||||||
|
animateClick = true,
|
||||||
} = props;
|
} = props;
|
||||||
const [isDown, setIsDown] = useState(false);
|
const [isDown, setIsDown] = useState(false);
|
||||||
|
|
||||||
const animProps = useSpring({
|
const [animProps] = useSpring(
|
||||||
...SPRING_DEFAULTS,
|
{
|
||||||
from: isDown ? { scale: 1 } : { scale: 0 },
|
config: SPRING_CONFIG,
|
||||||
to: isDown ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
|
to: isDown && animateClick ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
|
||||||
});
|
},
|
||||||
|
[visible, isDown, animateClick]
|
||||||
|
);
|
||||||
|
|
||||||
// Clicking button toggle playback
|
// Clicking button toggle playback
|
||||||
const onButtonClick = (event: React.MouseEvent) => {
|
const onButtonClick = useCallback(
|
||||||
event.stopPropagation();
|
(event: React.MouseEvent) => {
|
||||||
event.preventDefault();
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
onClick();
|
onClick();
|
||||||
};
|
},
|
||||||
|
[onClick]
|
||||||
|
);
|
||||||
|
|
||||||
// Keyboard playback toggle
|
// Keyboard playback toggle
|
||||||
const onButtonKeyDown = (event: React.KeyboardEvent) => {
|
const onButtonKeyDown = useCallback(
|
||||||
if (event.key !== 'Enter' && event.key !== 'Space') {
|
(event: React.KeyboardEvent) => {
|
||||||
return;
|
if (event.key !== 'Enter' && event.key !== 'Space') {
|
||||||
}
|
return;
|
||||||
event.stopPropagation();
|
}
|
||||||
event.preventDefault();
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
onClick();
|
onClick();
|
||||||
};
|
},
|
||||||
|
[onClick]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<animated.div style={animProps}>
|
<animated.div style={animProps}>
|
||||||
|
@ -193,7 +202,7 @@ const PlayedDot = ({
|
||||||
|
|
||||||
const [animProps] = useSpring(
|
const [animProps] = useSpring(
|
||||||
{
|
{
|
||||||
...SPRING_DEFAULTS,
|
config: SPRING_CONFIG,
|
||||||
from: { scale: start, opacity: start, width: start },
|
from: { scale: start, opacity: start, width: start },
|
||||||
to: { scale: end, opacity: end, width: end * DOT_DIV_WIDTH },
|
to: { scale: end, opacity: end, width: end * DOT_DIV_WIDTH },
|
||||||
onRest: () => {
|
onRest: () => {
|
||||||
|
@ -253,7 +262,6 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
onCorrupted,
|
onCorrupted,
|
||||||
onFirstPlayed,
|
|
||||||
computePeaks,
|
computePeaks,
|
||||||
setPlaybackRate,
|
setPlaybackRate,
|
||||||
loadAndPlayMessageAudio,
|
loadAndPlayMessageAudio,
|
||||||
|
@ -366,12 +374,6 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!played && isPlaying) {
|
|
||||||
onFirstPlayed();
|
|
||||||
}
|
|
||||||
}, [played, isPlaying, onFirstPlayed]);
|
|
||||||
|
|
||||||
// Clicking waveform moves playback head position and starts playback.
|
// Clicking waveform moves playback head position and starts playback.
|
||||||
const onWaveformClick = (event: React.MouseEvent) => {
|
const onWaveformClick = (event: React.MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -508,6 +510,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
variant="play"
|
variant="play"
|
||||||
mod="download"
|
mod="download"
|
||||||
label="MessageAudio--download"
|
label="MessageAudio--download"
|
||||||
|
animateClick={false}
|
||||||
onClick={kickOffAttachmentDownload}
|
onClick={kickOffAttachmentDownload}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -519,6 +522,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
variant="play"
|
variant="play"
|
||||||
mod={isPlaying ? 'pause' : 'play'}
|
mod={isPlaying ? 'pause' : 'play'}
|
||||||
label={isPlaying ? 'MessageAudio--pause' : 'MessageAudio--play'}
|
label={isPlaying ? 'MessageAudio--pause' : 'MessageAudio--play'}
|
||||||
|
animateClick={false}
|
||||||
onClick={toggleIsPlaying}
|
onClick={toggleIsPlaying}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -561,7 +565,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
variant="playback-rate"
|
variant="playback-rate"
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
label={(active && playbackRateLabels[active.playbackRate]) ?? ''}
|
label={(active && playbackRateLabels[active.playbackRate]) ?? ''}
|
||||||
visible={isPlaying && (!played || (played && !isPlayedDotVisible))}
|
visible={isPlaying && (!played || !isPlayedDotVisible)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (active) {
|
if (active) {
|
||||||
setPlaybackRate(
|
setPlaybackRate(
|
||||||
|
|
|
@ -65,6 +65,7 @@ export type PropsData = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
|
markViewed: (messageId: string) => void;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
MessagePropsType,
|
MessagePropsType,
|
||||||
| 'getPreferredBadge'
|
| 'getPreferredBadge'
|
||||||
|
@ -78,7 +79,6 @@ export type PropsBackboneActions = Pick<
|
||||||
| 'displayTapToViewMessage'
|
| 'displayTapToViewMessage'
|
||||||
| 'kickOffAttachmentDownload'
|
| 'kickOffAttachmentDownload'
|
||||||
| 'markAttachmentAsCorrupted'
|
| 'markAttachmentAsCorrupted'
|
||||||
| 'markViewed'
|
|
||||||
| 'openConversation'
|
| 'openConversation'
|
||||||
| 'openGiftBadge'
|
| 'openGiftBadge'
|
||||||
| 'openLink'
|
| 'openLink'
|
||||||
|
|
|
@ -22,11 +22,15 @@ import type {
|
||||||
import {
|
import {
|
||||||
SELECTED_CONVERSATION_CHANGED,
|
SELECTED_CONVERSATION_CHANGED,
|
||||||
setVoiceNotePlaybackRate,
|
setVoiceNotePlaybackRate,
|
||||||
|
markViewed,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import { globalMessageAudio } from '../../services/globalMessageAudio';
|
import { globalMessageAudio } from '../../services/globalMessageAudio';
|
||||||
|
import { isPlayed } from '../../types/Attachment';
|
||||||
|
import { getMessageIdForLogging } from '../../util/idForLogging';
|
||||||
|
import { getMessagePropStatus } from '../selectors/message';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -254,6 +258,33 @@ function loadAndPlayMessageAudio(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// mark the message as played
|
||||||
|
const message = getState().conversations.messagesLookup[id];
|
||||||
|
if (message) {
|
||||||
|
const messageIdForLogging = getMessageIdForLogging(message);
|
||||||
|
const status = getMessagePropStatus(message, message.conversationId);
|
||||||
|
|
||||||
|
if (message.type === 'incoming' || message.type === 'outgoing') {
|
||||||
|
if (!isPlayed(message.type, status, message.readStatus)) {
|
||||||
|
markViewed(id);
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
'audioPlayer.loadAndPlayMessageAudio: message already played',
|
||||||
|
{ message: messageIdForLogging }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`audioPlayer.loadAndPlayMessageAudio: message wrong type: ${message.type}`,
|
||||||
|
{ message: messageIdForLogging }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn('audioPlayer.loadAndPlayMessageAudio: message not found', {
|
||||||
|
message: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// set the playback rate to the stored value for the selected conversation
|
// set the playback rate to the stored value for the selected conversation
|
||||||
const conversationId = getSelectedConversationId(getState());
|
const conversationId = getSelectedConversationId(getState());
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
|
|
|
@ -76,6 +76,7 @@ import {
|
||||||
OneTimeModalState,
|
OneTimeModalState,
|
||||||
UsernameSaveState,
|
UsernameSaveState,
|
||||||
} from './conversationsEnums';
|
} from './conversationsEnums';
|
||||||
|
import { markViewed as messageUpdaterMarkViewed } from '../../services/MessageUpdater';
|
||||||
import { showToast } from '../../util/showToast';
|
import { showToast } from '../../util/showToast';
|
||||||
import { ToastFailedToDeleteUsername } from '../../components/ToastFailedToDeleteUsername';
|
import { ToastFailedToDeleteUsername } from '../../components/ToastFailedToDeleteUsername';
|
||||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
@ -83,8 +84,15 @@ import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
import type { NoopActionType } from './noop';
|
import type { NoopActionType } from './noop';
|
||||||
import { conversationJobQueue } from '../../jobs/conversationJobQueue';
|
import { conversationJobQueue } from '../../jobs/conversationJobQueue';
|
||||||
import type { TimelineMessageLoadingState } from '../../util/timelineUtil';
|
import type { TimelineMessageLoadingState } from '../../util/timelineUtil';
|
||||||
import { isGroup } from '../../util/whatTypeOfConversation';
|
import {
|
||||||
|
isDirectConversation,
|
||||||
|
isGroup,
|
||||||
|
} from '../../util/whatTypeOfConversation';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
||||||
|
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||||
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
import { isIncoming } from '../selectors/message';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -1003,6 +1011,56 @@ function generateNewGroupLink(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not an actual redux action creator, so it doesn't produce an action (or dispatch
|
||||||
|
* itself) because updates are managed through the backbone model, which will trigger
|
||||||
|
* necessary updates and refresh conversation_view.
|
||||||
|
*
|
||||||
|
* In practice, it's similar to an already-connected thunk action. Later on we will
|
||||||
|
* replace it with an actual action that fits in with the redux approach.
|
||||||
|
*/
|
||||||
|
export const markViewed = (messageId: string): void => {
|
||||||
|
const message = window.MessageController.getById(messageId);
|
||||||
|
if (!message) {
|
||||||
|
throw new Error(`markViewed: Message ${messageId} missing!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.get('readStatus') === ReadStatus.Viewed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderE164 = message.get('source');
|
||||||
|
const senderUuid = message.get('sourceUuid');
|
||||||
|
const timestamp = message.get('sent_at');
|
||||||
|
|
||||||
|
message.set(messageUpdaterMarkViewed(message.attributes, Date.now()));
|
||||||
|
|
||||||
|
if (isIncoming(message.attributes)) {
|
||||||
|
viewedReceiptsJobQueue.add({
|
||||||
|
viewedReceipt: {
|
||||||
|
messageId,
|
||||||
|
senderE164,
|
||||||
|
senderUuid,
|
||||||
|
timestamp,
|
||||||
|
isDirectConversation: isDirectConversation(
|
||||||
|
message.getConversation()?.attributes
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
viewSyncJobQueue.add({
|
||||||
|
viewSyncs: [
|
||||||
|
{
|
||||||
|
messageId,
|
||||||
|
senderE164,
|
||||||
|
senderUuid,
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function setAccessControlAddFromInviteLinkSetting(
|
function setAccessControlAddFromInviteLinkSetting(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
value: boolean
|
value: boolean
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { SmartMessageDetail } from '../smart/MessageDetail';
|
||||||
|
|
||||||
export const createMessageDetail = (
|
export const createMessageDetail = (
|
||||||
store: Store,
|
store: Store,
|
||||||
props: OwnProps
|
props: Omit<OwnProps, 'markViewed'>
|
||||||
): ReactElement => (
|
): ReactElement => (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<SmartMessageDetail {...props} />
|
<SmartMessageDetail {...props} />
|
||||||
|
|
|
@ -40,7 +40,6 @@ export type Props = {
|
||||||
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
|
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
|
||||||
kickOffAttachmentDownload(): void;
|
kickOffAttachmentDownload(): void;
|
||||||
onCorrupted(): void;
|
onCorrupted(): void;
|
||||||
onFirstPlayed(): void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (
|
const mapStateToProps = (
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { renderAudioAttachment } from './renderAudioAttachment';
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
import { renderReactionPicker } from './renderReactionPicker';
|
import { renderReactionPicker } from './renderReactionPicker';
|
||||||
import { getContactNameColorSelector } from '../selectors/conversations';
|
import { getContactNameColorSelector } from '../selectors/conversations';
|
||||||
|
import { markViewed } from '../ducks/conversations';
|
||||||
|
|
||||||
export { Contact } from '../../components/conversation/MessageDetail';
|
export { Contact } from '../../components/conversation/MessageDetail';
|
||||||
export type OwnProps = Omit<
|
export type OwnProps = Omit<
|
||||||
|
@ -25,6 +26,7 @@ export type OwnProps = Omit<
|
||||||
| 'renderEmojiPicker'
|
| 'renderEmojiPicker'
|
||||||
| 'renderReactionPicker'
|
| 'renderReactionPicker'
|
||||||
| 'theme'
|
| 'theme'
|
||||||
|
| 'markViewed'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const mapStateToProps = (
|
const mapStateToProps = (
|
||||||
|
@ -43,7 +45,6 @@ const mapStateToProps = (
|
||||||
displayTapToViewMessage,
|
displayTapToViewMessage,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
markViewed,
|
|
||||||
openConversation,
|
openConversation,
|
||||||
openGiftBadge,
|
openGiftBadge,
|
||||||
openLink,
|
openLink,
|
||||||
|
|
|
@ -50,6 +50,7 @@ import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||||
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
|
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
|
||||||
import type { WidthBreakpoint } from '../../components/_util';
|
import type { WidthBreakpoint } from '../../components/_util';
|
||||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||||
|
import { markViewed } from '../ducks/conversations';
|
||||||
|
|
||||||
type ExternalProps = {
|
type ExternalProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -76,7 +77,6 @@ export type TimelinePropsType = ExternalProps &
|
||||||
| 'loadOlderMessages'
|
| 'loadOlderMessages'
|
||||||
| 'markAttachmentAsCorrupted'
|
| 'markAttachmentAsCorrupted'
|
||||||
| 'markMessageRead'
|
| 'markMessageRead'
|
||||||
| 'markViewed'
|
|
||||||
| 'onBlock'
|
| 'onBlock'
|
||||||
| 'onBlockAndReportSpam'
|
| 'onBlockAndReportSpam'
|
||||||
| 'onDelete'
|
| 'onDelete'
|
||||||
|
@ -317,6 +317,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
renderContactSpoofingReviewDialog,
|
renderContactSpoofingReviewDialog,
|
||||||
renderHeroRow,
|
renderHeroRow,
|
||||||
renderTypingBubble,
|
renderTypingBubble,
|
||||||
|
markViewed,
|
||||||
...actions,
|
...actions,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,6 +29,8 @@ import * as GoogleChrome from '../util/GoogleChrome';
|
||||||
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
||||||
import { getValue } from '../RemoteConfig';
|
import { getValue } from '../RemoteConfig';
|
||||||
import { isRecord } from '../util/isRecord';
|
import { isRecord } from '../util/isRecord';
|
||||||
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
|
import type { MessageStatusType } from '../components/conversation/Message';
|
||||||
|
|
||||||
const MAX_WIDTH = 300;
|
const MAX_WIDTH = 300;
|
||||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
||||||
|
@ -652,6 +654,17 @@ export function isAudio(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPlayed(
|
||||||
|
direction: 'outgoing' | 'incoming',
|
||||||
|
status: MessageStatusType | undefined,
|
||||||
|
readStatus: ReadStatus | undefined
|
||||||
|
): boolean {
|
||||||
|
if (direction === 'outgoing') {
|
||||||
|
return status === 'viewed';
|
||||||
|
}
|
||||||
|
return readStatus === ReadStatus.Viewed;
|
||||||
|
}
|
||||||
|
|
||||||
export function canDisplayImage(
|
export function canDisplayImage(
|
||||||
attachments?: ReadonlyArray<AttachmentType>
|
attachments?: ReadonlyArray<AttachmentType>
|
||||||
): boolean {
|
): boolean {
|
||||||
|
|
|
@ -56,7 +56,6 @@ import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
||||||
import { createConversationView } from '../state/roots/createConversationView';
|
import { createConversationView } from '../state/roots/createConversationView';
|
||||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||||
import type { CompositionAPIType } from '../components/CompositionArea';
|
import type { CompositionAPIType } from '../components/CompositionArea';
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import { ToastBlocked } from '../components/ToastBlocked';
|
import { ToastBlocked } from '../components/ToastBlocked';
|
||||||
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
||||||
|
@ -85,12 +84,9 @@ import { ToastCannotOpenGiftBadge } from '../components/ToastCannotOpenGiftBadge
|
||||||
import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
|
import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
|
||||||
import { retryMessageSend } from '../util/retryMessageSend';
|
import { retryMessageSend } from '../util/retryMessageSend';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
import { markViewed } from '../services/MessageUpdater';
|
|
||||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||||
import { resolveAttachmentDraftData } from '../util/resolveAttachmentDraftData';
|
import { resolveAttachmentDraftData } from '../util/resolveAttachmentDraftData';
|
||||||
import { showToast } from '../util/showToast';
|
import { showToast } from '../util/showToast';
|
||||||
import { viewSyncJobQueue } from '../jobs/viewSyncJobQueue';
|
|
||||||
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
|
|
||||||
import { RecordingState } from '../state/ducks/audioRecorder';
|
import { RecordingState } from '../state/ducks/audioRecorder';
|
||||||
import { UUIDKind } from '../types/UUID';
|
import { UUIDKind } from '../types/UUID';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
|
@ -152,7 +148,6 @@ type MessageActionsType = {
|
||||||
options: Readonly<{ messageId: string }>
|
options: Readonly<{ messageId: string }>
|
||||||
) => unknown;
|
) => unknown;
|
||||||
markAttachmentAsCorrupted: (options: AttachmentOptions) => unknown;
|
markAttachmentAsCorrupted: (options: AttachmentOptions) => unknown;
|
||||||
markViewed: (messageId: string) => unknown;
|
|
||||||
openConversation: (conversationId: string, messageId?: string) => unknown;
|
openConversation: (conversationId: string, messageId?: string) => unknown;
|
||||||
openGiftBadge: (messageId: string) => unknown;
|
openGiftBadge: (messageId: string) => unknown;
|
||||||
openLink: (url: string) => unknown;
|
openLink: (url: string) => unknown;
|
||||||
|
@ -793,45 +788,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
}
|
}
|
||||||
message.markAttachmentAsCorrupted(options.attachment);
|
message.markAttachmentAsCorrupted(options.attachment);
|
||||||
};
|
};
|
||||||
const onMarkViewed = (messageId: string): void => {
|
|
||||||
const message = window.MessageController.getById(messageId);
|
|
||||||
if (!message) {
|
|
||||||
throw new Error(`onMarkViewed: Message ${messageId} missing!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.get('readStatus') === ReadStatus.Viewed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderE164 = message.get('source');
|
|
||||||
const senderUuid = message.get('sourceUuid');
|
|
||||||
const timestamp = message.get('sent_at');
|
|
||||||
|
|
||||||
message.set(markViewed(message.attributes, Date.now()));
|
|
||||||
|
|
||||||
if (isIncoming(message.attributes)) {
|
|
||||||
viewedReceiptsJobQueue.add({
|
|
||||||
viewedReceipt: {
|
|
||||||
messageId,
|
|
||||||
senderE164,
|
|
||||||
senderUuid,
|
|
||||||
timestamp,
|
|
||||||
isDirectConversation: isDirectConversation(this.model.attributes),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
viewSyncJobQueue.add({
|
|
||||||
viewSyncs: [
|
|
||||||
{
|
|
||||||
messageId,
|
|
||||||
senderE164,
|
|
||||||
senderUuid,
|
|
||||||
timestamp,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const showVisualAttachment = (options: {
|
const showVisualAttachment = (options: {
|
||||||
attachment: AttachmentType;
|
attachment: AttachmentType;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
@ -889,7 +846,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
downloadNewVersion,
|
downloadNewVersion,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
markViewed: onMarkViewed,
|
|
||||||
openConversation,
|
openConversation,
|
||||||
openGiftBadge,
|
openGiftBadge,
|
||||||
openLink,
|
openLink,
|
||||||
|
|
Loading…
Reference in New Issue