Audio Player: Various fixes (#2376)
This commit is contained in:
parent
1c2a9a4a3c
commit
bf4e2ed994
@ -13,14 +13,14 @@
|
||||
margin: 0.125rem;
|
||||
}
|
||||
|
||||
> .player-button {
|
||||
.player-button {
|
||||
--color-text-secondary: var(--color-primary);
|
||||
--color-text-secondary-rgb: var(--color-primary-shade-rgb);
|
||||
--color-primary-shade: var(--color-green);
|
||||
--color-white: var(--color-background-own);
|
||||
}
|
||||
|
||||
.player-button {
|
||||
margin: 0.125rem;
|
||||
|
||||
&.smaller i {
|
||||
font-size: 1.625rem;
|
||||
margin-top: -0.0625rem;
|
||||
@ -52,9 +52,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.volume-button {
|
||||
.volume-button-wrapper {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.volume-slider-wrapper {
|
||||
position: absolute;
|
||||
width: 8rem;
|
||||
top: 2.625rem;
|
||||
}
|
||||
|
||||
.volume-slider-spacer {
|
||||
position: absolute;
|
||||
@ -102,21 +110,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
.playback-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.playback-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 5rem;
|
||||
height: 4rem;
|
||||
z-index: calc(var(--z-menu-backdrop) + 1);
|
||||
}
|
||||
|
||||
.playback-button {
|
||||
overflow: visible;
|
||||
&.on-top {
|
||||
z-index: calc(var(--z-menu-backdrop) + 2);
|
||||
}
|
||||
|
||||
&.applied {
|
||||
--color-text-secondary: var(--color-primary);
|
||||
}
|
||||
|
||||
.playback-button-inner {
|
||||
transition: 0.2s color ease-in-out;
|
||||
transition: 0.15s color ease-out;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
border: 2px solid;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&.small {
|
||||
transform: scale(0.83);
|
||||
}
|
||||
|
||||
&.tiny {
|
||||
transform: scale(0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ type StateProps = {
|
||||
chat?: ApiChat;
|
||||
volume: number;
|
||||
playbackRate: number;
|
||||
isPlaybackRateActive?: boolean;
|
||||
isMuted: boolean;
|
||||
};
|
||||
|
||||
@ -56,6 +57,10 @@ const PLAYBACK_RATES: Record<number, number> = {
|
||||
1.5: 1.4,
|
||||
2: 1.8,
|
||||
};
|
||||
const PLAYBACK_RATE_VALUES = Object.keys(PLAYBACK_RATES).sort().map(Number);
|
||||
|
||||
const REGULAR_PLAYBACK_RATE = 1;
|
||||
const DEFAULT_FAST_PLAYBACK_RATE = 2;
|
||||
|
||||
const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
@ -65,6 +70,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
chat,
|
||||
volume,
|
||||
playbackRate,
|
||||
isPlaybackRateActive,
|
||||
isMuted,
|
||||
}) => {
|
||||
const {
|
||||
@ -143,37 +149,62 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
setAudioPlayerMuted({ isMuted: !isMuted });
|
||||
}, [isMuted, setAudioPlayerMuted, toggleMuted]);
|
||||
|
||||
const updatePlaybackRate = useCallback((newRate: number) => {
|
||||
const updatePlaybackRate = useCallback((newRate: number, isActive = true) => {
|
||||
const rate = PLAYBACK_RATES[newRate];
|
||||
setAudioPlayerPlaybackRate({ playbackRate: rate });
|
||||
setPlaybackRate(rate);
|
||||
const shouldBeActive = newRate !== REGULAR_PLAYBACK_RATE && isActive;
|
||||
setAudioPlayerPlaybackRate({ playbackRate: rate, isPlaybackRateActive: shouldBeActive });
|
||||
setPlaybackRate(shouldBeActive ? rate : REGULAR_PLAYBACK_RATE);
|
||||
}, [setAudioPlayerPlaybackRate, setPlaybackRate]);
|
||||
|
||||
const handlePlaybackClick = useCallback(() => {
|
||||
if (isContextMenuOpen) return;
|
||||
updatePlaybackRate(playbackRate === 1 ? 2 : 1);
|
||||
}, [isContextMenuOpen, playbackRate, updatePlaybackRate]);
|
||||
handleContextMenuClose();
|
||||
const oldRate = Number(Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0])
|
||||
|| REGULAR_PLAYBACK_RATE;
|
||||
const newIsActive = !isPlaybackRateActive;
|
||||
|
||||
updatePlaybackRate(
|
||||
newIsActive && oldRate === REGULAR_PLAYBACK_RATE ? DEFAULT_FAST_PLAYBACK_RATE : oldRate,
|
||||
newIsActive,
|
||||
);
|
||||
}, [handleContextMenuClose, isPlaybackRateActive, playbackRate, updatePlaybackRate]);
|
||||
|
||||
const PlaybackRateButton = useCallback(() => {
|
||||
const displayRate = Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0] || 1;
|
||||
const displayRate = Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0]
|
||||
|| REGULAR_PLAYBACK_RATE;
|
||||
const text = `${playbackRate === REGULAR_PLAYBACK_RATE ? DEFAULT_FAST_PLAYBACK_RATE : displayRate}Х`;
|
||||
return (
|
||||
<Button
|
||||
round
|
||||
className={buildClassName('playback-button', playbackRate !== 1 && 'applied')}
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel="Playback Rate"
|
||||
ripple={!isMobile}
|
||||
onClick={handlePlaybackClick}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<span className="playback-button-inner">
|
||||
{playbackRate === 1 ? 2 : displayRate}Х
|
||||
</span>
|
||||
</Button>
|
||||
<div className="playback-wrapper">
|
||||
{isContextMenuOpen && <div className="playback-backdrop" onClick={handleContextMenuClose} />}
|
||||
|
||||
<Button
|
||||
round
|
||||
className={buildClassName(
|
||||
'playback-button', isPlaybackRateActive && 'applied', isContextMenuOpen && 'on-top',
|
||||
)}
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel="Playback Rate"
|
||||
ripple={!isMobile}
|
||||
onMouseEnter={handleContextMenu}
|
||||
onClick={handlePlaybackClick}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<span className={buildClassName(
|
||||
'playback-button-inner',
|
||||
text.length === 4 && 'small',
|
||||
text.length === 5 && 'tiny',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}, [handleBeforeContextMenu, handleContextMenu, handlePlaybackClick, isMobile, playbackRate]);
|
||||
}, [
|
||||
handleBeforeContextMenu, handleContextMenu, handleContextMenuClose, handlePlaybackClick, isContextMenuOpen,
|
||||
isMobile, isPlaybackRateActive, playbackRate,
|
||||
]);
|
||||
|
||||
const volumeIcon = useMemo(() => {
|
||||
if (volume === 0 || isMuted) return 'icon-muted';
|
||||
@ -230,24 +261,28 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
<i className="icon-skip-next" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
round
|
||||
className="player-button volume-button"
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel="Volume"
|
||||
noPreventDefault
|
||||
>
|
||||
<i className={volumeIcon} onClick={handleVolumeClick} />
|
||||
<div className="volume-button-wrapper">
|
||||
<Button
|
||||
round
|
||||
className="player-button volume-button"
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel="Volume"
|
||||
onClick={handleVolumeClick}
|
||||
ripple={!isMobile}
|
||||
>
|
||||
<i className={volumeIcon} />
|
||||
</Button>
|
||||
|
||||
{!IS_IOS && (
|
||||
<>
|
||||
<div className="volume-slider-wrapper">
|
||||
<div className="volume-slider-spacer" />
|
||||
<div className="volume-slider">
|
||||
<RangeSlider bold value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{shouldRenderPlaybackButton && (
|
||||
<DropdownMenu
|
||||
@ -258,12 +293,11 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
trigger={PlaybackRateButton}
|
||||
onClose={handleContextMenuClose}
|
||||
onHide={handleContextMenuHide}
|
||||
onMouseEnterBackdrop={handleContextMenuClose}
|
||||
>
|
||||
{renderPlaybackRateMenuItem(0.5, playbackRate, updatePlaybackRate)}
|
||||
{renderPlaybackRateMenuItem(0.75, playbackRate, updatePlaybackRate)}
|
||||
{renderPlaybackRateMenuItem(1, playbackRate, updatePlaybackRate)}
|
||||
{renderPlaybackRateMenuItem(1.5, playbackRate, updatePlaybackRate)}
|
||||
{renderPlaybackRateMenuItem(2, playbackRate, updatePlaybackRate)}
|
||||
{PLAYBACK_RATE_VALUES.map((rate) => {
|
||||
return renderPlaybackRateMenuItem(rate, playbackRate, updatePlaybackRate, isPlaybackRateActive);
|
||||
})}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
@ -303,13 +337,19 @@ function renderVoice(subtitle: string, senderName?: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderPlaybackRateMenuItem(rate: number, currentRate: number, onClick: (rate: number) => void) {
|
||||
function renderPlaybackRateMenuItem(
|
||||
rate: number, currentRate: number, onClick: (rate: number) => void,
|
||||
isPlaybackRateActive?: boolean,
|
||||
) {
|
||||
const isSelected = (currentRate === PLAYBACK_RATES[rate] && isPlaybackRateActive)
|
||||
|| (rate === REGULAR_PLAYBACK_RATE && !isPlaybackRateActive);
|
||||
return (
|
||||
<MenuItem
|
||||
key={rate}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onClick(rate)}
|
||||
icon={currentRate === PLAYBACK_RATES[rate] ? 'check' : undefined}
|
||||
customIcon={currentRate !== PLAYBACK_RATES[rate] ? <i className="icon-placeholder" /> : undefined}
|
||||
icon={isSelected ? 'check' : undefined}
|
||||
customIcon={!isSelected ? <i className="icon-placeholder" /> : undefined}
|
||||
>
|
||||
{rate}X
|
||||
</MenuItem>
|
||||
@ -320,13 +360,16 @@ export default withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const sender = selectSender(global, message);
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const { volume, playbackRate, isMuted } = selectTabState(global).audioPlayer;
|
||||
const {
|
||||
volume, playbackRate, isMuted, isPlaybackRateActive,
|
||||
} = selectTabState(global).audioPlayer;
|
||||
|
||||
return {
|
||||
sender,
|
||||
chat,
|
||||
volume,
|
||||
playbackRate,
|
||||
isPlaybackRateActive,
|
||||
isMuted,
|
||||
};
|
||||
},
|
||||
|
||||
@ -46,7 +46,7 @@ export type OwnProps = {
|
||||
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
||||
onMouseEnter?: NoneToVoidFunction;
|
||||
onMouseEnter?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
||||
onMouseLeave?: NoneToVoidFunction;
|
||||
onFocus?: NoneToVoidFunction;
|
||||
onTransitionEnd?: NoneToVoidFunction;
|
||||
|
||||
@ -16,6 +16,7 @@ type OwnProps = {
|
||||
onClose?: NoneToVoidFunction;
|
||||
onHide?: NoneToVoidFunction;
|
||||
onTransitionEnd?: NoneToVoidFunction;
|
||||
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
@ -30,6 +31,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
onOpen,
|
||||
onClose,
|
||||
onTransitionEnd,
|
||||
onMouseEnterBackdrop,
|
||||
onHide,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -88,6 +90,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
onClose={handleClose}
|
||||
shouldSkipTransition={forceOpen}
|
||||
onCloseAnimationEnd={onHide}
|
||||
onMouseEnterBackdrop={onMouseEnterBackdrop}
|
||||
>
|
||||
{children}
|
||||
</Menu>
|
||||
|
||||
@ -40,6 +40,7 @@ type OwnProps = {
|
||||
onCloseAnimationEnd?: () => void;
|
||||
onClose: () => void;
|
||||
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
withPortal?: boolean;
|
||||
children: React.ReactNode;
|
||||
@ -71,6 +72,7 @@ const Menu: FC<OwnProps> = ({
|
||||
onMouseLeave,
|
||||
shouldSkipTransition,
|
||||
withPortal,
|
||||
onMouseEnterBackdrop,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
let menuRef = useRef<HTMLDivElement>(null);
|
||||
@ -143,7 +145,11 @@ const Menu: FC<OwnProps> = ({
|
||||
>
|
||||
{isOpen && (
|
||||
// This only prevents click events triggering on underlying elements
|
||||
<div className="backdrop" onMouseDown={preventMessageInputBlurWithBubbling} />
|
||||
<div
|
||||
className="backdrop"
|
||||
onMouseDown={preventMessageInputBlurWithBubbling}
|
||||
onMouseEnter={onMouseEnterBackdrop}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
role="presentation"
|
||||
|
||||
@ -44,5 +44,6 @@
|
||||
border-radius: 50%;
|
||||
transform: scale(0);
|
||||
animation: ripple-animation 700ms;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,13 +212,14 @@ addActionHandler('setAudioPlayerVolume', (global, actions, payload): ActionRetur
|
||||
|
||||
addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
playbackRate, tabId = getCurrentTabId(),
|
||||
playbackRate, isPlaybackRateActive, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
return updateTabState(global, {
|
||||
audioPlayer: {
|
||||
...selectTabState(global, tabId).audioPlayer,
|
||||
playbackRate,
|
||||
isPlaybackRateActive,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
@ -256,6 +257,7 @@ addActionHandler('closeAudioPlayer', (global, actions, payload): ActionReturnTyp
|
||||
audioPlayer: {
|
||||
volume: tabState.audioPlayer.volume,
|
||||
playbackRate: tabState.audioPlayer.playbackRate,
|
||||
isPlaybackRateActive: undefined,
|
||||
isMuted: tabState.audioPlayer.isMuted,
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
@ -321,6 +321,7 @@ export type TabState = {
|
||||
origin?: AudioOrigin;
|
||||
volume: number;
|
||||
playbackRate: number;
|
||||
isPlaybackRateActive?: boolean;
|
||||
isMuted: boolean;
|
||||
};
|
||||
|
||||
@ -1708,6 +1709,7 @@ export interface ActionPayloads {
|
||||
} & WithTabId;
|
||||
setAudioPlayerPlaybackRate: {
|
||||
playbackRate: number;
|
||||
isPlaybackRateActive?: boolean;
|
||||
} & WithTabId;
|
||||
setAudioPlayerMuted: {
|
||||
isMuted: boolean;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user