Voice Notes mini-player: Show with no conversation, fix spacing

This commit is contained in:
Scott Nonnenberg 2023-03-20 11:03:21 -07:00 committed by GitHub
parent 9015837b2e
commit 75d5e81013
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 159 additions and 87 deletions

View file

@ -24,6 +24,8 @@
align-items: center;
gap: 18px;
padding: 8px 16px;
margin-top: calc(52px + var(--title-bar-drag-area-height));
text-align: left;
@include light-theme {
background-color: $color-gray-02;
@ -32,6 +34,11 @@
background-color: $color-gray-75;
}
&--flow {
margin-top: 0;
position: relative;
}
&__playback-button {
@include button-reset;

View file

@ -25,6 +25,7 @@ export type PropsType = {
renderConversationView: () => JSX.Element;
renderCustomizingPreferredReactionsModal: () => JSX.Element;
renderLeftPane: () => JSX.Element;
renderMiniPlayer: (options: { shouldFlow: boolean }) => JSX.Element;
scrollToMessage: (conversationId: string, messageId: string) => unknown;
selectedConversationId?: string;
selectedMessage?: string;
@ -42,6 +43,7 @@ export function Inbox({
renderConversationView,
renderCustomizingPreferredReactionsModal,
renderLeftPane,
renderMiniPlayer,
scrollToMessage,
selectedConversationId,
selectedMessage,
@ -219,6 +221,7 @@ export function Inbox({
)}
{!prevConversationId && (
<div className="no-conversation-open">
{renderMiniPlayer({ shouldFlow: false })}
<div className="module-splash-screen__logo module-img--128 module-logo-blue" />
<h3>{i18n('welcomeToSignal')}</h3>
<p className="whats-new-placeholder">

View file

@ -1,6 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import classnames from 'classnames';
import React, { useCallback } from 'react';
import type { LocalizerType } from '../types/Util';
import { durationToPlaybackText } from '../util/durationToPlaybackText';
@ -22,6 +23,8 @@ export type Props = Readonly<{
duration: number | undefined;
playbackRate: number;
state: PlayerState;
// if false or not provided, position:absolute. Otherwise, it's position: relative
shouldFlow?: boolean;
onPlay: () => void;
onPause: () => void;
onPlaybackRate: (rate: number) => void;
@ -35,6 +38,7 @@ export function MiniPlayer({
currentTime,
duration,
playbackRate,
shouldFlow,
onPlay,
onPause,
onPlaybackRate,
@ -79,7 +83,12 @@ export function MiniPlayer({
}
return (
<div className="MiniPlayer">
<div
className={classnames(
'MiniPlayer',
shouldFlow ? 'MiniPlayer--flow' : null
)}
>
<PlaybackButton
context="incoming"
variant="mini"

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { SmartMiniPlayer } from '../../state/smart/MiniPlayer';
export type PropsType = {
conversationId: string;
@ -87,7 +86,6 @@ export function ConversationView({
{renderConversationHeader()}
</div>
<div className="ConversationView__pane main panel">
<SmartMiniPlayer />
<div className="ConversationView__timeline--container">
<div aria-live="polite" className="ConversationView__timeline">
{renderTimeline()}

View file

@ -335,9 +335,6 @@ const renderItem = ({
getPreferredBadge={() => undefined}
id=""
isSelected={false}
renderEmojiPicker={() => <div />}
renderReactionPicker={() => <div />}
item={items[messageId]}
i18n={i18n}
interactionMode="keyboard"
isNextItemCallingNotification={false}
@ -345,11 +342,14 @@ const renderItem = ({
containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint}
conversationId=""
item={items[messageId]}
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
renderContact={() => '*ContactName*'}
renderEmojiPicker={() => <div />}
renderReactionPicker={() => <div />}
renderUniversalTimerNotification={() => (
<div>*UniversalTimerNotification*</div>
)}
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
shouldCollapseAbove={false}
shouldCollapseBelow={false}
shouldHideMetadata={false}
@ -436,6 +436,9 @@ const renderTypingBubble = () => (
sharedGroupNames={[]}
/>
);
const renderMiniPlayer = () => (
<div>If active, this is where smart mini player would be</div>
);
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
discardMessages: action('discardMessages'),
@ -455,6 +458,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
messageChangeCounter: 0,
scrollToIndex: overrideProps.scrollToIndex,
scrollToIndexCounter: 0,
shouldShowMiniPlayer: Boolean(overrideProps.shouldShowMiniPlayer),
totalUnseen: number('totalUnseen', overrideProps.totalUnseen || 0),
oldestUnseenIndex:
number('oldestUnseenIndex', overrideProps.oldestUnseenIndex || 0) ||
@ -466,6 +470,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
id: uuid(),
renderItem,
renderHeroRow,
renderMiniPlayer,
renderTypingBubble,
renderContactSpoofingReviewDialog,
isSomeoneTyping: overrideProps.isSomeoneTyping || false,
@ -620,3 +625,12 @@ export function WithSameNameInGroupConversationWarning(): JSX.Element {
WithSameNameInGroupConversationWarning.story = {
name: 'With "same name in group conversation" warning',
};
export function WithJustMiniPlayer(): JSX.Element {
const props = useProps({
shouldShowMiniPlayer: true,
items: [],
});
return <Timeline {...props} />;
}

View file

@ -100,8 +100,9 @@ type PropsHousekeepingType = {
isSomeoneTyping: boolean;
unreadCount?: number;
selectedMessageId?: string;
invitedContactsForNewlyCreatedGroup: Array<ConversationType>;
selectedMessageId?: string;
shouldShowMiniPlayer: boolean;
warning?: WarningType;
contactSpoofingReview?: ContactSpoofingReviewPropType;
@ -120,6 +121,10 @@ type PropsHousekeepingType = {
i18n: LocalizerType;
theme: ThemeType;
renderContactSpoofingReviewDialog: (
props: SmartContactSpoofingReviewDialogPropsType
) => JSX.Element;
renderHeroRow: (id: string) => JSX.Element;
renderItem: (props: {
containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
@ -130,11 +135,8 @@ type PropsHousekeepingType = {
previousMessageId: undefined | string;
unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement;
}) => JSX.Element;
renderHeroRow: (id: string) => JSX.Element;
renderMiniPlayer: (options: { shouldFlow: boolean }) => JSX.Element;
renderTypingBubble: (id: string) => JSX.Element;
renderContactSpoofingReviewDialog: (
props: SmartContactSpoofingReviewDialogPropsType
) => JSX.Element;
};
export type PropsActionsType = {
@ -758,9 +760,11 @@ export class Timeline extends React.Component<
renderContactSpoofingReviewDialog,
renderHeroRow,
renderItem,
renderMiniPlayer,
renderTypingBubble,
reviewGroupMemberNameCollision,
reviewMessageRequestNameCollision,
shouldShowMiniPlayer,
theme,
totalUnseen,
unreadCount,
@ -890,72 +894,74 @@ export class Timeline extends React.Component<
}
const warning = Timeline.getWarning(this.props, this.state);
let timelineWarning: ReactNode;
if (warning) {
let text: ReactChild;
let headerElements: ReactNode;
if (warning || shouldShowMiniPlayer) {
let text: ReactChild | undefined;
let onClose: () => void;
switch (warning.type) {
case ContactSpoofingType.DirectConversationWithSameTitle:
text = (
<Intl
i18n={i18n}
id="ContactSpoofing__same-name"
components={{
link: (
<TimelineWarning.Link
onClick={() => {
reviewMessageRequestNameCollision({
safeConversationId: warning.safeConversation.id,
});
}}
>
{i18n('ContactSpoofing__same-name__link')}
</TimelineWarning.Link>
),
}}
/>
);
onClose = () => {
this.setState({
hasDismissedDirectContactSpoofingWarning: true,
});
};
break;
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
const { groupNameCollisions } = warning;
text = (
<Intl
i18n={i18n}
id="ContactSpoofing__same-name-in-group"
components={{
count: Object.values(groupNameCollisions)
.reduce(
(result, conversations) => result + conversations.length,
0
)
.toString(),
link: (
<TimelineWarning.Link
onClick={() => {
reviewGroupMemberNameCollision(id);
}}
>
{i18n('ContactSpoofing__same-name-in-group__link')}
</TimelineWarning.Link>
),
}}
/>
);
onClose = () => {
acknowledgeGroupMemberNameCollisions(id, groupNameCollisions);
};
break;
if (warning) {
switch (warning.type) {
case ContactSpoofingType.DirectConversationWithSameTitle:
text = (
<Intl
i18n={i18n}
id="ContactSpoofing__same-name"
components={{
link: (
<TimelineWarning.Link
onClick={() => {
reviewMessageRequestNameCollision({
safeConversationId: warning.safeConversation.id,
});
}}
>
{i18n('ContactSpoofing__same-name__link')}
</TimelineWarning.Link>
),
}}
/>
);
onClose = () => {
this.setState({
hasDismissedDirectContactSpoofingWarning: true,
});
};
break;
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
const { groupNameCollisions } = warning;
text = (
<Intl
i18n={i18n}
id="ContactSpoofing__same-name-in-group"
components={{
count: Object.values(groupNameCollisions)
.reduce(
(result, conversations) => result + conversations.length,
0
)
.toString(),
link: (
<TimelineWarning.Link
onClick={() => {
reviewGroupMemberNameCollision(id);
}}
>
{i18n('ContactSpoofing__same-name-in-group__link')}
</TimelineWarning.Link>
),
}}
/>
);
onClose = () => {
acknowledgeGroupMemberNameCollisions(id, groupNameCollisions);
};
break;
}
default:
throw missingCaseError(warning);
}
default:
throw missingCaseError(warning);
}
timelineWarning = (
headerElements = (
<Measure
bounds
onResize={({ bounds }) => {
@ -968,12 +974,15 @@ export class Timeline extends React.Component<
>
{({ measureRef }) => (
<TimelineWarnings ref={measureRef}>
<TimelineWarning i18n={i18n} onClose={onClose}>
<TimelineWarning.IconContainer>
<TimelineWarning.GenericIcon />
</TimelineWarning.IconContainer>
<TimelineWarning.Text>{text}</TimelineWarning.Text>
</TimelineWarning>
{renderMiniPlayer({ shouldFlow: true })}
{text && (
<TimelineWarning i18n={i18n} onClose={onClose}>
<TimelineWarning.IconContainer>
<TimelineWarning.GenericIcon />
</TimelineWarning.IconContainer>
<TimelineWarning.Text>{text}</TimelineWarning.Text>
</TimelineWarning>
)}
</TimelineWarnings>
)}
</Measure>
@ -1044,7 +1053,7 @@ export class Timeline extends React.Component<
onKeyDown={this.handleKeyDown}
ref={measureRef}
>
{timelineWarning}
{headerElements}
{floatingHeader}

View file

@ -190,6 +190,7 @@ const {
} = window.Signal.Data;
const FIVE_MINUTES = MINUTE * 5;
const FETCH_TIMEOUT = SECOND * 30;
const JOB_REPORTING_THRESHOLD_MS = 25;
const SEND_REPORTING_THRESHOLD_MS = 25;
@ -1366,10 +1367,12 @@ export class ConversationModel extends window.Backbone
e164,
reason: 'ConversationModel.onNewMessage',
});
const typingToken = `${source?.id}.${sourceDevice}`;
if (source) {
const typingToken = `${source.id}.${sourceDevice}`;
// Clear typing indicator for a given contact if we receive a message from them
this.clearContactTypingTimer(typingToken);
// Clear typing indicator for a given contact if we receive a message from them
this.clearContactTypingTimer(typingToken);
}
// If it's a group story reply or a story message, we don't want to update
// the last message or add new messages to redux.
@ -1451,10 +1454,18 @@ export class ConversationModel extends window.Backbone
resolvePromise = resolve;
});
let timeout: NodeJS.Timeout;
const finish = () => {
resolvePromise();
clearTimeout(timeout);
this.inProgressFetch = undefined;
};
timeout = setTimeout(() => {
log.warn(
`setInProgressFetch(${this.idForLogging()}): Calling finish manually after timeout`
);
finish();
}, FETCH_TIMEOUT);
return finish;
}
@ -5542,7 +5553,7 @@ export class ConversationModel extends window.Backbone
return;
}
const typingToken = `${senderId}.${senderDevice}`;
const typingToken = `${sender.id}.${senderDevice}`;
this.contactTypingTimers = this.contactTypingTimers || {};
const record = this.contactTypingTimers[typingToken];

View file

@ -14,6 +14,7 @@ import { SmartLeftPane } from './LeftPane';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions';
import { SmartMiniPlayer } from './MiniPlayer';
function renderConversationView() {
return <SmartConversationView />;
@ -23,6 +24,10 @@ function renderCustomizingPreferredReactionsModal() {
return <SmartCustomizingPreferredReactionsModal />;
}
function renderMiniPlayer(options: { shouldFlow: boolean }) {
return <SmartMiniPlayer {...options} />;
}
function renderLeftPane() {
return <SmartLeftPane />;
}
@ -59,6 +64,7 @@ export function SmartInbox(): JSX.Element {
renderCustomizingPreferredReactionsModal
}
renderLeftPane={renderLeftPane}
renderMiniPlayer={renderMiniPlayer}
scrollToMessage={scrollToMessage}
selectedConversationId={selectedConversationId}
selectedMessage={selectedMessage}

View file

@ -4,6 +4,7 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { MiniPlayer, PlayerState } from '../../components/MiniPlayer';
import type { Props as DumbProps } from '../../components/MiniPlayer';
import {
AudioPlayerContent,
useAudioPlayerActions,
@ -14,13 +15,15 @@ import {
} from '../selectors/audioPlayer';
import { getIntl } from '../selectors/user';
type Props = Pick<DumbProps, 'shouldFlow'>;
/**
* Wires the dispatch props and shows/hides the MiniPlayer
*
* It also triggers side-effecting actions (actual playback) in response to changes in
* the state
*/
export function SmartMiniPlayer(): JSX.Element | null {
export function SmartMiniPlayer({ shouldFlow }: Props): JSX.Element | null {
const i18n = useSelector(getIntl);
const active = useSelector(selectAudioPlayerActive);
const getVoiceNoteTitle = useSelector(selectVoiceNoteTitle);
@ -56,6 +59,7 @@ export function SmartMiniPlayer(): JSX.Element | null {
onPause={handlePause}
onPlaybackRate={setPlaybackRate}
onClose={unloadMessageAudio}
shouldFlow={shouldFlow}
state={state}
currentTime={active.currentTime}
duration={active.duration}

View file

@ -26,6 +26,7 @@ import {
getInvitedContactsForNewlyCreatedGroup,
getSelectedMessage,
} from '../selectors/conversations';
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
import { SmartTimelineItem } from './TimelineItem';
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
@ -46,6 +47,7 @@ import { ContactSpoofingType } from '../../util/contactSpoofing';
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
import type { WidthBreakpoint } from '../../components/_util';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { SmartMiniPlayer } from './MiniPlayer';
type ExternalProps = {
id: string;
@ -93,6 +95,9 @@ function renderContactSpoofingReviewDialog(
function renderHeroRow(id: string): JSX.Element {
return <SmartHeroRow id={id} />;
}
function renderMiniPlayer(options: { shouldFlow: boolean }): JSX.Element {
return <SmartMiniPlayer {...options} />;
}
function renderTypingBubble(id: string): JSX.Element {
return <SmartTypingBubble id={id} />;
}
@ -227,6 +232,8 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const getTimestampForMessage = (messageId: string): undefined | number =>
getMessages(state)[messageId]?.timestamp;
const shouldShowMiniPlayer = Boolean(selectAudioPlayerActive(state));
return {
id,
...pick(conversation, ['unreadCount', 'isGroupV1AndDisabled']),
@ -237,9 +244,11 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
),
isSomeoneTyping: Boolean(conversation.typingContactId),
...conversationMessages,
invitedContactsForNewlyCreatedGroup:
getInvitedContactsForNewlyCreatedGroup(state),
selectedMessageId: selectedMessage ? selectedMessage.id : undefined,
shouldShowMiniPlayer,
warning: getWarning(conversation, state),
contactSpoofingReview: getContactSpoofingReview(id, state),
@ -248,9 +257,11 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
theme: getTheme(state),
renderItem,
renderContactSpoofingReviewDialog,
renderHeroRow,
renderItem,
renderMiniPlayer,
renderTypingBubble,
};
};