Voice-note animation fixes

This commit is contained in:
Alvaro 2022-10-03 17:43:44 -06:00 committed by GitHub
parent 56f8842ed2
commit 458eb2ea81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 147 additions and 104 deletions

View File

@ -162,6 +162,7 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
if (!playing) {
audio.play();
setPlaying(true);
setPlayed(true);
}
if (!Number.isNaN(audio.duration)) {
@ -195,10 +196,6 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
? { playing, playbackRate, currentTime, duration: audio.duration }
: undefined;
const setPlayedAction = () => {
setPlayed(true);
};
return (
<MessageAudio
{...props}
@ -208,7 +205,6 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
active={active}
played={_played}
loadAndPlayMessageAudio={loadAndPlayMessageAudio}
onFirstPlayed={setPlayedAction}
setIsPlaying={setIsPlayingAction}
setPlaybackRate={setPlaybackRateAction}
setCurrentTime={setCurrentTimeAction}

View File

@ -18,7 +18,7 @@ import type {
} from '../../state/ducks/conversations';
import type { ViewStoryActionCreatorType } from '../../state/ducks/stories';
import type { TimelineItemType } from './TimelineItem';
import { ReadStatus } from '../../messages/MessageReadStatus';
import type { ReadStatus } from '../../messages/MessageReadStatus';
import { Avatar, AvatarSize } from '../Avatar';
import { AvatarSpacer } from '../AvatarSpacer';
import { Spinner } from '../Spinner';
@ -61,6 +61,7 @@ import {
isImageAttachment,
isVideo,
isGIF,
isPlayed,
} from '../../types/Attachment';
import type { EmbeddedContactType } from '../../types/EmbeddedContact';
@ -179,7 +180,6 @@ export type AudioAttachmentProps = {
kickOffAttachmentDownload(): void;
onCorrupted(): void;
onFirstPlayed(): void;
};
export enum GiftBadgeStates {
@ -902,7 +902,6 @@ export class Message extends React.PureComponent<Props, State> {
isSticker,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
markViewed,
quote,
readStatus,
reducedMotion,
@ -1017,19 +1016,7 @@ export class Message extends React.PureComponent<Props, State> {
}
}
if (isAudio(attachments)) {
let played: boolean;
switch (direction) {
case 'outgoing':
played = status === 'viewed';
break;
case 'incoming':
played = readStatus === ReadStatus.Viewed;
break;
default:
log.error(missingCaseError(direction));
played = false;
break;
}
const played = isPlayed(direction, status, readStatus);
return renderAudioAttachment({
i18n,
@ -1064,9 +1051,6 @@ export class Message extends React.PureComponent<Props, State> {
messageId: id,
});
},
onFirstPlayed() {
markViewed(id);
},
});
}
const { pending, fileName, fileSize, contentType } = firstAttachment;

View File

@ -1,7 +1,7 @@
// Copyright 2021-2022 Signal Messenger, LLC
// 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 { noop } from 'lodash';
import { animated, useSpring } from '@react-spring/web';
@ -38,7 +38,6 @@ export type OwnProps = Readonly<{
timestamp: number;
kickOffAttachmentDownload(): void;
onCorrupted(): void;
onFirstPlayed(): void;
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
}>;
@ -63,6 +62,7 @@ type ButtonProps = {
mod?: string;
label: string;
visible?: boolean;
animateClick?: boolean;
onClick: () => void;
onMouseDown?: () => void;
onMouseUp?: () => void;
@ -91,7 +91,7 @@ const BIG_INCREMENT = 5;
const PLAYBACK_RATES = [1, 1.5, 2, 0.5];
const SPRING_DEFAULTS = {
const SPRING_CONFIG = {
mass: 0.5,
tension: 350,
friction: 20,
@ -131,33 +131,42 @@ const Button: React.FC<ButtonProps> = props => {
children,
onClick,
visible = true,
animateClick = true,
} = props;
const [isDown, setIsDown] = useState(false);
const animProps = useSpring({
...SPRING_DEFAULTS,
from: isDown ? { scale: 1 } : { scale: 0 },
to: isDown ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
});
const [animProps] = useSpring(
{
config: SPRING_CONFIG,
to: isDown && animateClick ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
},
[visible, isDown, animateClick]
);
// Clicking button toggle playback
const onButtonClick = (event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
const onButtonClick = useCallback(
(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onClick();
};
onClick();
},
[onClick]
);
// Keyboard playback toggle
const onButtonKeyDown = (event: React.KeyboardEvent) => {
if (event.key !== 'Enter' && event.key !== 'Space') {
return;
}
event.stopPropagation();
event.preventDefault();
const onButtonKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key !== 'Enter' && event.key !== 'Space') {
return;
}
event.stopPropagation();
event.preventDefault();
onClick();
};
onClick();
},
[onClick]
);
return (
<animated.div style={animProps}>
@ -193,7 +202,7 @@ const PlayedDot = ({
const [animProps] = useSpring(
{
...SPRING_DEFAULTS,
config: SPRING_CONFIG,
from: { scale: start, opacity: start, width: start },
to: { scale: end, opacity: end, width: end * DOT_DIV_WIDTH },
onRest: () => {
@ -253,7 +262,6 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
kickOffAttachmentDownload,
onCorrupted,
onFirstPlayed,
computePeaks,
setPlaybackRate,
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.
const onWaveformClick = (event: React.MouseEvent) => {
event.preventDefault();
@ -508,6 +510,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
variant="play"
mod="download"
label="MessageAudio--download"
animateClick={false}
onClick={kickOffAttachmentDownload}
/>
);
@ -519,6 +522,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
variant="play"
mod={isPlaying ? 'pause' : 'play'}
label={isPlaying ? 'MessageAudio--pause' : 'MessageAudio--play'}
animateClick={false}
onClick={toggleIsPlaying}
/>
);
@ -561,7 +565,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
variant="playback-rate"
i18n={i18n}
label={(active && playbackRateLabels[active.playbackRate]) ?? ''}
visible={isPlaying && (!played || (played && !isPlayedDotVisible))}
visible={isPlaying && (!played || !isPlayedDotVisible)}
onClick={() => {
if (active) {
setPlaybackRate(

View File

@ -65,6 +65,7 @@ export type PropsData = {
i18n: LocalizerType;
theme: ThemeType;
getPreferredBadge: PreferredBadgeSelectorType;
markViewed: (messageId: string) => void;
} & Pick<
MessagePropsType,
| 'getPreferredBadge'
@ -78,7 +79,6 @@ export type PropsBackboneActions = Pick<
| 'displayTapToViewMessage'
| 'kickOffAttachmentDownload'
| 'markAttachmentAsCorrupted'
| 'markViewed'
| 'openConversation'
| 'openGiftBadge'
| 'openLink'

View File

@ -22,11 +22,15 @@ import type {
import {
SELECTED_CONVERSATION_CHANGED,
setVoiceNotePlaybackRate,
markViewed,
} from './conversations';
import * as log from '../../logging/log';
import { strictAssert } from '../../util/assert';
import { globalMessageAudio } from '../../services/globalMessageAudio';
import { isPlayed } from '../../types/Attachment';
import { getMessageIdForLogging } from '../../util/idForLogging';
import { getMessagePropStatus } from '../selectors/message';
// 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
const conversationId = getSelectedConversationId(getState());
if (conversationId) {

View File

@ -76,6 +76,7 @@ import {
OneTimeModalState,
UsernameSaveState,
} from './conversationsEnums';
import { markViewed as messageUpdaterMarkViewed } from '../../services/MessageUpdater';
import { showToast } from '../../util/showToast';
import { ToastFailedToDeleteUsername } from '../../components/ToastFailedToDeleteUsername';
import { useBoundActions } from '../../hooks/useBoundActions';
@ -83,8 +84,15 @@ import { useBoundActions } from '../../hooks/useBoundActions';
import type { NoopActionType } from './noop';
import { conversationJobQueue } from '../../jobs/conversationJobQueue';
import type { TimelineMessageLoadingState } from '../../util/timelineUtil';
import { isGroup } from '../../util/whatTypeOfConversation';
import {
isDirectConversation,
isGroup,
} from '../../util/whatTypeOfConversation';
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
@ -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(
conversationId: string,
value: boolean

View File

@ -12,7 +12,7 @@ import { SmartMessageDetail } from '../smart/MessageDetail';
export const createMessageDetail = (
store: Store,
props: OwnProps
props: Omit<OwnProps, 'markViewed'>
): ReactElement => (
<Provider store={store}>
<SmartMessageDetail {...props} />

View File

@ -40,7 +40,6 @@ export type Props = {
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
kickOffAttachmentDownload(): void;
onCorrupted(): void;
onFirstPlayed(): void;
};
const mapStateToProps = (

View File

@ -14,6 +14,7 @@ import { renderAudioAttachment } from './renderAudioAttachment';
import { renderEmojiPicker } from './renderEmojiPicker';
import { renderReactionPicker } from './renderReactionPicker';
import { getContactNameColorSelector } from '../selectors/conversations';
import { markViewed } from '../ducks/conversations';
export { Contact } from '../../components/conversation/MessageDetail';
export type OwnProps = Omit<
@ -25,6 +26,7 @@ export type OwnProps = Omit<
| 'renderEmojiPicker'
| 'renderReactionPicker'
| 'theme'
| 'markViewed'
>;
const mapStateToProps = (
@ -43,7 +45,6 @@ const mapStateToProps = (
displayTapToViewMessage,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
markViewed,
openConversation,
openGiftBadge,
openLink,

View File

@ -50,6 +50,7 @@ import { ContactSpoofingType } from '../../util/contactSpoofing';
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
import type { WidthBreakpoint } from '../../components/_util';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { markViewed } from '../ducks/conversations';
type ExternalProps = {
id: string;
@ -76,7 +77,6 @@ export type TimelinePropsType = ExternalProps &
| 'loadOlderMessages'
| 'markAttachmentAsCorrupted'
| 'markMessageRead'
| 'markViewed'
| 'onBlock'
| 'onBlockAndReportSpam'
| 'onDelete'
@ -317,6 +317,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
renderContactSpoofingReviewDialog,
renderHeroRow,
renderTypingBubble,
markViewed,
...actions,
};
};

View File

@ -29,6 +29,8 @@ import * as GoogleChrome from '../util/GoogleChrome';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { getValue } from '../RemoteConfig';
import { isRecord } from '../util/isRecord';
import { ReadStatus } from '../messages/MessageReadStatus';
import type { MessageStatusType } from '../components/conversation/Message';
const MAX_WIDTH = 300;
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(
attachments?: ReadonlyArray<AttachmentType>
): boolean {

View File

@ -56,7 +56,6 @@ import type { EmbeddedContactType } from '../types/EmbeddedContact';
import { createConversationView } from '../state/roots/createConversationView';
import { AttachmentToastType } from '../types/AttachmentToastType';
import type { CompositionAPIType } from '../components/CompositionArea';
import { ReadStatus } from '../messages/MessageReadStatus';
import { SignalService as Proto } from '../protobuf';
import { ToastBlocked } from '../components/ToastBlocked';
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
@ -85,12 +84,9 @@ import { ToastCannotOpenGiftBadge } from '../components/ToastCannotOpenGiftBadge
import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
import { retryMessageSend } from '../util/retryMessageSend';
import { isNotNil } from '../util/isNotNil';
import { markViewed } from '../services/MessageUpdater';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { resolveAttachmentDraftData } from '../util/resolveAttachmentDraftData';
import { showToast } from '../util/showToast';
import { viewSyncJobQueue } from '../jobs/viewSyncJobQueue';
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
import { RecordingState } from '../state/ducks/audioRecorder';
import { UUIDKind } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
@ -152,7 +148,6 @@ type MessageActionsType = {
options: Readonly<{ messageId: string }>
) => unknown;
markAttachmentAsCorrupted: (options: AttachmentOptions) => unknown;
markViewed: (messageId: string) => unknown;
openConversation: (conversationId: string, messageId?: string) => unknown;
openGiftBadge: (messageId: string) => unknown;
openLink: (url: string) => unknown;
@ -793,45 +788,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
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: {
attachment: AttachmentType;
messageId: string;
@ -889,7 +846,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
downloadNewVersion,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
markViewed: onMarkViewed,
openConversation,
openGiftBadge,
openLink,