Tile styling and early behaviour

This commit is contained in:
Travis Ralston 2021-06-23 19:34:34 -06:00
parent aaec9857fd
commit 9c752680ba
12 changed files with 310 additions and 30 deletions

View file

@ -37,8 +37,10 @@
@import "./structures/_ViewSource.scss";
@import "./structures/auth/_CompleteSecurity.scss";
@import "./structures/auth/_Login.scss";
@import "./views/audio_messages/_AudioPlayer.scss";
@import "./views/audio_messages/_PlayPauseButton.scss";
@import "./views/audio_messages/_PlaybackContainer.scss";
@import "./views/audio_messages/_SeekBar.scss";
@import "./views/audio_messages/_Waveform.scss";
@import "./views/auth/_AuthBody.scss";
@import "./views/auth/_AuthButtons.scss";
@ -159,7 +161,6 @@
@import "./views/messages/_CreateEvent.scss";
@import "./views/messages/_DateSeparator.scss";
@import "./views/messages/_EventTileBubble.scss";
@import "./views/messages/_MAudioBody.scss";
@import "./views/messages/_MEmoteBody.scss";
@import "./views/messages/_MFileBody.scss";
@import "./views/messages/_MImageBody.scss";

View file

@ -0,0 +1,68 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_AudioPlayer_container {
padding: 16px 12px 11px 12px;
width: 267px;
.mx_AudioPlayer_primaryContainer {
display: flex;
.mx_PlayPauseButton {
margin-right: 8px;
}
.mx_AudioPlayer_mediaInfo {
flex: 1;
overflow: hidden; // makes the ellipsis on the file name work
& > * {
display: block;
}
.mx_AudioPlayer_mediaName {
color: $primary-fg-color;
font-size: $font-15px;
line-height: $font-15px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-bottom: 4px; // mimics the line-height differences in the Figma
}
.mx_AudioPlayer_byline {
font-size: $font-12px;
line-height: $font-12px;
}
}
}
.mx_AudioPlayer_seek {
display: flex;
align-items: center;
.mx_SeekBar {
flex: 1;
}
.mx_Clock {
width: $font-42px; // we're not using a monospace font, so fake it
min-width: $font-42px; // for flexbox
padding-left: 4px; // isolate from seek bar
text-align: right;
}
}
}

View file

@ -18,6 +18,8 @@ limitations under the License.
position: relative;
width: 32px;
height: 32px;
min-width: 32px; // for when the button is used in a flexbox
min-height: 32px; // for when the button is used in a flexbox
border-radius: 32px;
background-color: $voice-playback-button-bg-color;

View file

@ -0,0 +1,89 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// CSS inspiration from:
// * https://www.w3schools.com/howto/howto_js_rangeslider.asp
// * https://stackoverflow.com/a/28283806
// * https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/
.mx_SeekBar {
// Dev note: we deliberately do not have the -ms-track (and friends) selectors because we don't
// need to support IE.
appearance: none; // default style override
width: 100%;
height: 1px;
background: $quaternary-fg-color;
outline: none; // remove blue selection border
position: relative; // for progress bar support later on
&::-webkit-slider-thumb {
appearance: none; // default style override
// Dev note: This needs to be duplicated with the -moz-range-thumb selector
// because otherwise Edge (webkit) will fail to see the styles and just refuse
// to apply them.
width: 8px;
height: 8px;
border-radius: 8px;
background-color: $tertiary-fg-color;
cursor: pointer;
}
&::-moz-range-thumb {
width: 8px;
height: 8px;
border-radius: 8px;
background-color: $tertiary-fg-color;
cursor: pointer;
// Firefox adds a border on the thumb
border: none;
}
// This is for webkit support, but we can't limit the functionality of it to just webkit
// browsers. Firefox responds to webkit-prefixed values now, which means we can't use media
// or support queries to selectively apply the rule. An upside is that this CSS doesn't work
// in firefox, so it's just wasted CPU/GPU time.
&::before { // ::before to ensure it ends up under the thumb
content: '';
background-color: $tertiary-fg-color;
// Absolute positioning to ensure it overlaps with the existing bar
position: absolute;
top: 0;
left: 0;
// Sizing to match the bar
width: 100%;
height: 1px;
// And finally dynamic width without overly hurting the rendering engine.
transform-origin: 0 100%;
transform: scaleX(var(--fillTo));
}
// This is firefox's built-in support for the above, with 100% less hacks.
&::-moz-range-progress {
background-color: $tertiary-fg-color;
height: 1px;
}
&:disabled {
opacity: 0.5;
}
}

View file

@ -1,19 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MAudioBody {
display: inline-block; // makes the playback controls magically line up
}

View file

@ -20,6 +20,9 @@ $tertiary-fg-color: $primary-fg-color;
$primary-bg-color: $bg-color;
$muted-fg-color: $header-panel-text-primary-color;
// Legacy theme backports
$quaternary-fg-color: #6F7882;
// used for dialog box text
$light-fg-color: $header-panel-text-secondary-color;

View file

@ -28,6 +28,9 @@ $tertiary-fg-color: $primary-fg-color;
$primary-bg-color: #ffffff;
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
// Legacy theme backports
$quaternary-fg-color: #C1C6CD;
// used for dialog box text
$light-fg-color: #747474;

View file

@ -22,11 +22,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils";
import DurationClock from "./DurationClock";
import { Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler";
import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
mediaName: string;
}
interface IState {
@ -83,13 +88,21 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
tabIndex={-1} // prevent tabbing into the button
ref={this.playPauseRef}
/>
<div className='mx_AudioPlayer_mediaInfo'>
<span className='mx_AudioPlayer_mediaName'>
{this.props.mediaName || _t("Unnamed audio")}
</span>
<div className='mx_AudioPlayer_byline'>
<DurationClock playback={this.props.playback} />
&nbsp; {/* easiest way to introduce a gap between the components */}
{ this.renderFileSize() }
</div>
</div>
<span>TODO: Seek bar</span>
</div>
<div className='mx_AudioPlayer_seek'>
<SeekBar playback={this.props.playback} tabIndex={-1} playbackPhase={this.state.playbackPhase} />
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
</div>
</div>
}
}

View file

@ -22,6 +22,11 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps {
playback: Playback;
// The default number of seconds to show when the playback has completed or
// has not started. Not used during playback, even when paused. Defaults to
// clip duration length.
defaultDisplaySeconds?: number;
}
interface IState {
@ -64,8 +69,12 @@ export default class PlaybackClock extends React.PureComponent<IProps, IState> {
public render() {
let seconds = this.state.seconds;
if (this.state.playbackPhase === PlaybackState.Stopped) {
if (Number.isFinite(this.props.defaultDisplaySeconds)) {
seconds = this.props.defaultDisplaySeconds;
} else {
seconds = this.state.durationSeconds;
}
}
return <Clock seconds={seconds} />;
}
}

View file

@ -0,0 +1,110 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { ChangeEvent, createRef, CSSProperties, ReactNode, RefObject } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils";
import DurationClock from "./DurationClock";
import { Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import { percentageOf } from "../../../utils/numbers";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
// Tab index for the underlying component. Useful if the seek bar is in a managed state.
// Defaults to zero.
tabIndex?: number;
playbackPhase: PlaybackState;
}
interface IState {
percentage: number;
}
interface ISeekCSS extends CSSProperties {
'--fillTo': number;
}
@replaceableComponent("views.audio_messages.SeekBar")
export default class SeekBar extends React.PureComponent<IProps, IState> {
// We use an animation frame request to avoid overly spamming prop updates, even if we aren't
// really using anything demanding on the CSS front.
private animationFrameFn = new MarkedExecution(
() => this.doUpdate(),
() => requestAnimationFrame(() => this.animationFrameFn.trigger()));
public static defaultProps = {
tabIndex: 0,
};
constructor(props: IProps) {
super(props);
this.state = {
percentage: 0,
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.clockInfo.liveData.onUpdate(() => this.animationFrameFn.mark());
}
private doUpdate() {
this.setState({
percentage: percentageOf(
this.props.playback.clockInfo.timeSeconds,
0,
this.props.playback.clockInfo.durationSeconds),
});
}
public left() {
console.log("@@ LEFT");
}
public right() {
console.log("@@ RIGHT");
}
private onChange = (ev: ChangeEvent<HTMLInputElement>) => {
console.log('@@ CHANGE', ev.target.value);
};
public render(): ReactNode {
// We use a range input to avoid having to re-invent accessibility handling on
// a custom set of divs.
return <input
type="range"
className='mx_SeekBar'
tabIndex={this.props.tabIndex}
onChange={this.onChange}
min={0}
max={1}
value={this.state.percentage}
step={0.001}
style={{'--fillTo': this.state.percentage} as ISeekCSS}
disabled={this.props.playbackPhase === PlaybackState.Decoding}
/>;
}
}

View file

@ -102,7 +102,7 @@ export default class MAudioBody extends React.PureComponent<IProps, IState> {
// At this point we should have a playable state
return (
<span className="mx_MAudioBody">
<AudioPlayer playback={this.state.playback} />
<AudioPlayer playback={this.state.playback} mediaName={this.props.mxEvent.getContent().body} />
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
</span>
)

View file

@ -918,8 +918,6 @@
"Silence call": "Silence call",
"Decline": "Decline",
"Accept": "Accept",
"Pause": "Pause",
"Play": "Play",
"The other party cancelled the verification.": "The other party cancelled the verification.",
"Verified!": "Verified!",
"You've successfully verified this user.": "You've successfully verified this user.",
@ -1870,7 +1868,7 @@
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
"Encryption not enabled": "Encryption not enabled",
"The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.",
"Error decrypting audio": "Error decrypting audio",
"Error processing audio message": "Error processing audio message",
"React": "React",
"Edit": "Edit",
"Retry": "Retry",
@ -2602,6 +2600,9 @@
"Use email or phone to optionally be discoverable by existing contacts.": "Use email or phone to optionally be discoverable by existing contacts.",
"Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.",
"Sign in with SSO": "Sign in with SSO",
"Unnamed audio": "Unnamed audio",
"Pause": "Pause",
"Play": "Play",
"Couldn't load page": "Couldn't load page",
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
"You must join the room to see its files": "You must join the room to see its files",