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

View file

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

View file

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

View file

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

View file

@ -335,9 +335,6 @@ const renderItem = ({
getPreferredBadge={() => undefined} getPreferredBadge={() => undefined}
id="" id=""
isSelected={false} isSelected={false}
renderEmojiPicker={() => <div />}
renderReactionPicker={() => <div />}
item={items[messageId]}
i18n={i18n} i18n={i18n}
interactionMode="keyboard" interactionMode="keyboard"
isNextItemCallingNotification={false} isNextItemCallingNotification={false}
@ -345,11 +342,14 @@ const renderItem = ({
containerElementRef={containerElementRef} containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint} containerWidthBreakpoint={containerWidthBreakpoint}
conversationId="" conversationId=""
item={items[messageId]}
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
renderContact={() => '*ContactName*'} renderContact={() => '*ContactName*'}
renderEmojiPicker={() => <div />}
renderReactionPicker={() => <div />}
renderUniversalTimerNotification={() => ( renderUniversalTimerNotification={() => (
<div>*UniversalTimerNotification*</div> <div>*UniversalTimerNotification*</div>
)} )}
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
shouldCollapseAbove={false} shouldCollapseAbove={false}
shouldCollapseBelow={false} shouldCollapseBelow={false}
shouldHideMetadata={false} shouldHideMetadata={false}
@ -436,6 +436,9 @@ const renderTypingBubble = () => (
sharedGroupNames={[]} sharedGroupNames={[]}
/> />
); );
const renderMiniPlayer = () => (
<div>If active, this is where smart mini player would be</div>
);
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
discardMessages: action('discardMessages'), discardMessages: action('discardMessages'),
@ -455,6 +458,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
messageChangeCounter: 0, messageChangeCounter: 0,
scrollToIndex: overrideProps.scrollToIndex, scrollToIndex: overrideProps.scrollToIndex,
scrollToIndexCounter: 0, scrollToIndexCounter: 0,
shouldShowMiniPlayer: Boolean(overrideProps.shouldShowMiniPlayer),
totalUnseen: number('totalUnseen', overrideProps.totalUnseen || 0), totalUnseen: number('totalUnseen', overrideProps.totalUnseen || 0),
oldestUnseenIndex: oldestUnseenIndex:
number('oldestUnseenIndex', overrideProps.oldestUnseenIndex || 0) || number('oldestUnseenIndex', overrideProps.oldestUnseenIndex || 0) ||
@ -466,6 +470,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
id: uuid(), id: uuid(),
renderItem, renderItem,
renderHeroRow, renderHeroRow,
renderMiniPlayer,
renderTypingBubble, renderTypingBubble,
renderContactSpoofingReviewDialog, renderContactSpoofingReviewDialog,
isSomeoneTyping: overrideProps.isSomeoneTyping || false, isSomeoneTyping: overrideProps.isSomeoneTyping || false,
@ -620,3 +625,12 @@ export function WithSameNameInGroupConversationWarning(): JSX.Element {
WithSameNameInGroupConversationWarning.story = { WithSameNameInGroupConversationWarning.story = {
name: 'With "same name in group conversation" warning', 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; isSomeoneTyping: boolean;
unreadCount?: number; unreadCount?: number;
selectedMessageId?: string;
invitedContactsForNewlyCreatedGroup: Array<ConversationType>; invitedContactsForNewlyCreatedGroup: Array<ConversationType>;
selectedMessageId?: string;
shouldShowMiniPlayer: boolean;
warning?: WarningType; warning?: WarningType;
contactSpoofingReview?: ContactSpoofingReviewPropType; contactSpoofingReview?: ContactSpoofingReviewPropType;
@ -120,6 +121,10 @@ type PropsHousekeepingType = {
i18n: LocalizerType; i18n: LocalizerType;
theme: ThemeType; theme: ThemeType;
renderContactSpoofingReviewDialog: (
props: SmartContactSpoofingReviewDialogPropsType
) => JSX.Element;
renderHeroRow: (id: string) => JSX.Element;
renderItem: (props: { renderItem: (props: {
containerElementRef: RefObject<HTMLElement>; containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint; containerWidthBreakpoint: WidthBreakpoint;
@ -130,11 +135,8 @@ type PropsHousekeepingType = {
previousMessageId: undefined | string; previousMessageId: undefined | string;
unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement; unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement;
}) => JSX.Element; }) => JSX.Element;
renderHeroRow: (id: string) => JSX.Element; renderMiniPlayer: (options: { shouldFlow: boolean }) => JSX.Element;
renderTypingBubble: (id: string) => JSX.Element; renderTypingBubble: (id: string) => JSX.Element;
renderContactSpoofingReviewDialog: (
props: SmartContactSpoofingReviewDialogPropsType
) => JSX.Element;
}; };
export type PropsActionsType = { export type PropsActionsType = {
@ -758,9 +760,11 @@ export class Timeline extends React.Component<
renderContactSpoofingReviewDialog, renderContactSpoofingReviewDialog,
renderHeroRow, renderHeroRow,
renderItem, renderItem,
renderMiniPlayer,
renderTypingBubble, renderTypingBubble,
reviewGroupMemberNameCollision, reviewGroupMemberNameCollision,
reviewMessageRequestNameCollision, reviewMessageRequestNameCollision,
shouldShowMiniPlayer,
theme, theme,
totalUnseen, totalUnseen,
unreadCount, unreadCount,
@ -890,72 +894,74 @@ export class Timeline extends React.Component<
} }
const warning = Timeline.getWarning(this.props, this.state); const warning = Timeline.getWarning(this.props, this.state);
let timelineWarning: ReactNode; let headerElements: ReactNode;
if (warning) { if (warning || shouldShowMiniPlayer) {
let text: ReactChild; let text: ReactChild | undefined;
let onClose: () => void; let onClose: () => void;
switch (warning.type) { if (warning) {
case ContactSpoofingType.DirectConversationWithSameTitle: switch (warning.type) {
text = ( case ContactSpoofingType.DirectConversationWithSameTitle:
<Intl text = (
i18n={i18n} <Intl
id="ContactSpoofing__same-name" i18n={i18n}
components={{ id="ContactSpoofing__same-name"
link: ( components={{
<TimelineWarning.Link link: (
onClick={() => { <TimelineWarning.Link
reviewMessageRequestNameCollision({ onClick={() => {
safeConversationId: warning.safeConversation.id, reviewMessageRequestNameCollision({
}); safeConversationId: warning.safeConversation.id,
}} });
> }}
{i18n('ContactSpoofing__same-name__link')} >
</TimelineWarning.Link> {i18n('ContactSpoofing__same-name__link')}
), </TimelineWarning.Link>
}} ),
/> }}
); />
onClose = () => { );
this.setState({ onClose = () => {
hasDismissedDirectContactSpoofingWarning: true, this.setState({
}); hasDismissedDirectContactSpoofingWarning: true,
}; });
break; };
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: { break;
const { groupNameCollisions } = warning; case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
text = ( const { groupNameCollisions } = warning;
<Intl text = (
i18n={i18n} <Intl
id="ContactSpoofing__same-name-in-group" i18n={i18n}
components={{ id="ContactSpoofing__same-name-in-group"
count: Object.values(groupNameCollisions) components={{
.reduce( count: Object.values(groupNameCollisions)
(result, conversations) => result + conversations.length, .reduce(
0 (result, conversations) => result + conversations.length,
) 0
.toString(), )
link: ( .toString(),
<TimelineWarning.Link link: (
onClick={() => { <TimelineWarning.Link
reviewGroupMemberNameCollision(id); onClick={() => {
}} reviewGroupMemberNameCollision(id);
> }}
{i18n('ContactSpoofing__same-name-in-group__link')} >
</TimelineWarning.Link> {i18n('ContactSpoofing__same-name-in-group__link')}
), </TimelineWarning.Link>
}} ),
/> }}
); />
onClose = () => { );
acknowledgeGroupMemberNameCollisions(id, groupNameCollisions); onClose = () => {
}; acknowledgeGroupMemberNameCollisions(id, groupNameCollisions);
break; };
break;
}
default:
throw missingCaseError(warning);
} }
default:
throw missingCaseError(warning);
} }
timelineWarning = ( headerElements = (
<Measure <Measure
bounds bounds
onResize={({ bounds }) => { onResize={({ bounds }) => {
@ -968,12 +974,15 @@ export class Timeline extends React.Component<
> >
{({ measureRef }) => ( {({ measureRef }) => (
<TimelineWarnings ref={measureRef}> <TimelineWarnings ref={measureRef}>
<TimelineWarning i18n={i18n} onClose={onClose}> {renderMiniPlayer({ shouldFlow: true })}
<TimelineWarning.IconContainer> {text && (
<TimelineWarning.GenericIcon /> <TimelineWarning i18n={i18n} onClose={onClose}>
</TimelineWarning.IconContainer> <TimelineWarning.IconContainer>
<TimelineWarning.Text>{text}</TimelineWarning.Text> <TimelineWarning.GenericIcon />
</TimelineWarning> </TimelineWarning.IconContainer>
<TimelineWarning.Text>{text}</TimelineWarning.Text>
</TimelineWarning>
)}
</TimelineWarnings> </TimelineWarnings>
)} )}
</Measure> </Measure>
@ -1044,7 +1053,7 @@ export class Timeline extends React.Component<
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
ref={measureRef} ref={measureRef}
> >
{timelineWarning} {headerElements}
{floatingHeader} {floatingHeader}

View file

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

View file

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

View file

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

View file

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