Merge branch 'develop' into fix-pip-color

This commit is contained in:
Šimon Brandner 2021-04-23 14:40:58 +02:00
commit ec908bc8be
No known key found for this signature in database
GPG key ID: 9760693FDD98A790
23 changed files with 465 additions and 157 deletions

View file

@ -35,7 +35,7 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpacePanel_spaceTreeWrapper { .mx_SpacePanel_spaceTreeWrapper {
flex: 1; flex: 1;
overflow-y: scroll; padding: 8px 8px 16px 0;
} }
.mx_SpacePanel_toggleCollapse { .mx_SpacePanel_toggleCollapse {
@ -59,11 +59,10 @@ $activeBorderColor: $secondary-fg-color;
margin: 0; margin: 0;
list-style: none; list-style: none;
padding: 0; padding: 0;
> .mx_SpaceItem {
padding-left: 16px; padding-left: 16px;
} }
.mx_AutoHideScrollbar {
padding: 8px 0 16px;
} }
.mx_SpaceButton_toggleCollapse { .mx_SpaceButton_toggleCollapse {

View file

@ -224,35 +224,6 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_FacePile_faces { .mx_FacePile_faces {
cursor: pointer; cursor: pointer;
> span:hover {
.mx_BaseAvatar {
filter: brightness(0.8);
}
}
> span:first-child {
position: relative;
.mx_BaseAvatar {
filter: brightness(0.8);
}
&::before {
content: "";
z-index: 1;
position: absolute;
top: 0;
left: 0;
height: 30px;
width: 30px;
background: #ffffff; // white icon fill
mask-position: center;
mask-size: 24px;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
}
}
} }
} }

View file

@ -20,7 +20,7 @@ limitations under the License.
flex-direction: row-reverse; flex-direction: row-reverse;
vertical-align: middle; vertical-align: middle;
> span + span { > .mx_FacePile_face + .mx_FacePile_face {
margin-right: -8px; margin-right: -8px;
} }
@ -31,9 +31,32 @@ limitations under the License.
.mx_BaseAvatar_initial { .mx_BaseAvatar_initial {
margin: 1px; // to offset the border on the image margin: 1px; // to offset the border on the image
} }
.mx_FacePile_more {
position: relative;
border-radius: 100%;
width: 30px;
height: 30px;
background-color: $groupFilterPanel-bg-color;
&::before {
content: "";
z-index: 1;
position: absolute;
top: 0;
left: 0;
height: inherit;
width: inherit;
background: $tertiary-fg-color;
mask-position: center;
mask-size: 20px;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
}
}
} }
> span { .mx_FacePile_summary {
margin-left: 12px; margin-left: 12px;
font-size: $font-14px; font-size: $font-14px;
line-height: $font-24px; line-height: $font-24px;

View file

@ -85,7 +85,7 @@ $dialog-close-fg-color: #9fa9ba;
$dialog-background-bg-color: $header-panel-bg-color; $dialog-background-bg-color: $header-panel-bg-color;
$lightbox-background-bg-color: #000; $lightbox-background-bg-color: #000;
$lightbox-background-bg-opacity: 85%; $lightbox-background-bg-opacity: 0.85;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #21262c; $settings-profile-placeholder-bg-color: #21262c;

View file

@ -83,7 +83,7 @@ $dialog-close-fg-color: #9fa9ba;
$dialog-background-bg-color: $header-panel-bg-color; $dialog-background-bg-color: $header-panel-bg-color;
$lightbox-background-bg-color: #000; $lightbox-background-bg-color: #000;
$lightbox-background-bg-opacity: 85%; $lightbox-background-bg-opacity: 0.85;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7; $settings-profile-placeholder-bg-color: #e7e7e7;

View file

@ -127,7 +127,7 @@ $dialog-close-fg-color: #c1c1c1;
$dialog-background-bg-color: #e9e9e9; $dialog-background-bg-color: #e9e9e9;
$lightbox-background-bg-color: #000; $lightbox-background-bg-color: #000;
$lightbox-background-bg-opacity: 95%; $lightbox-background-bg-opacity: 0.95;
$imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel: rgba(0, 0, 0, 0.7);
$imagebody-giflabel-border: rgba(0, 0, 0, 0.2); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2);

View file

@ -118,7 +118,7 @@ $dialog-close-fg-color: #c1c1c1;
$dialog-background-bg-color: #e9e9e9; $dialog-background-bg-color: #e9e9e9;
$lightbox-background-bg-color: #000; $lightbox-background-bg-color: #000;
$lightbox-background-bg-opacity: 95%; $lightbox-background-bg-opacity: 0.95;
$imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel: rgba(0, 0, 0, 0.7);
$imagebody-giflabel-border: rgba(0, 0, 0, 0.2); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2);

View file

@ -129,4 +129,30 @@ declare global {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/columnNumber // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/columnNumber
columnNumber?: number; columnNumber?: number;
} }
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
interface AudioWorkletProcessor {
readonly port: MessagePort;
process(
inputs: Float32Array[][],
outputs: Float32Array[][],
parameters: Record<string, Float32Array>
): boolean;
}
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
const AudioWorkletProcessor: {
prototype: AudioWorkletProcessor;
new (options?: AudioWorkletNodeOptions): AudioWorkletProcessor;
};
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
function registerProcessor(
name: string,
processorCtor: (new (
options?: AudioWorkletNodeOptions
) => AudioWorkletProcessor) & {
parameterDescriptors?: AudioParamDescriptor[];
}
);
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { HTMLAttributes } from "react"; import React, { HTMLAttributes, ReactNode, useContext } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { sortBy } from "lodash"; import { sortBy } from "lodash";
@ -24,6 +24,7 @@ import { _t } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import TextWithTooltip from "../elements/TextWithTooltip"; import TextWithTooltip from "../elements/TextWithTooltip";
import { useRoomMembers } from "../../../hooks/useRoomMembers"; import { useRoomMembers } from "../../../hooks/useRoomMembers";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
const DEFAULT_NUM_FACES = 5; const DEFAULT_NUM_FACES = 5;
@ -36,6 +37,7 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> {
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length; const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => { const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => {
const cli = useContext(MatrixClientContext);
let members = useRoomMembers(room); let members = useRoomMembers(room);
// sort users with an explicit avatar first // sort users with an explicit avatar first
@ -46,21 +48,42 @@ const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, .
// sort known users first // sort known users first
iteratees.unshift(member => isKnownMember(member)); iteratees.unshift(member => isKnownMember(member));
} }
if (members.length < 1) return null;
const shownMembers = sortBy(members, iteratees).slice(0, numShown); // exclude ourselves from the shown members list
return <div {...props} className="mx_FacePile"> const shownMembers = sortBy(members.filter(m => m.userId !== cli.getUserId()), iteratees).slice(0, numShown);
<div className="mx_FacePile_faces"> if (shownMembers.length < 1) return null;
{ shownMembers.map(member => {
return <TextWithTooltip key={member.userId} tooltip={member.name}> // We reverse the order of the shown faces in CSS to simplify their visual overlap,
<MemberAvatar member={member} width={28} height={28} /> // reverse members in tooltip order to make the order between the two match up.
</TextWithTooltip>; const commaSeparatedMembers = shownMembers.map(m => m.rawDisplayName).reverse().join(", ");
}) }
let tooltip: ReactNode;
if (props.onClick) {
tooltip = <div>
<div className="mx_Tooltip_title">
{ _t("View all %(count)s members", { count: members.length }) }
</div> </div>
{ onlyKnownUsers && <span> <div className="mx_Tooltip_sub">
{ _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers }) }
</div>
</div>;
} else {
tooltip = _t("%(count)s members including %(commaSeparatedMembers)s", {
count: members.length,
commaSeparatedMembers,
});
}
return <div {...props} className="mx_FacePile">
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}>
{ members.length > numShown ? <span className="mx_FacePile_face mx_FacePile_more" /> : null }
{ shownMembers.map(m =>
<MemberAvatar key={m.userId} member={m} width={28} height={28} className="mx_FacePile_face" /> )}
</TextWithTooltip>
{ onlyKnownUsers && <span className="mx_FacePile_summary">
{ _t("%(count)s people you know have already joined", { count: members.length }) } { _t("%(count)s people you know have already joined", { count: members.length }) }
</span> } </span> }
</div> </div>;
}; };
export default FacePile; export default FacePile;

View file

@ -32,13 +32,14 @@ import dis from '../../../dispatcher/dispatcher';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {normalizeWheelEvent} from "../../../utils/Mouse";
const MIN_ZOOM = 100; const MIN_ZOOM = 100;
const MAX_ZOOM = 300; const MAX_ZOOM = 300;
// This is used for the buttons // This is used for the buttons
const ZOOM_STEP = 10; const ZOOM_STEP = 10;
// This is used for mouse wheel events // This is used for mouse wheel events
const ZOOM_COEFFICIENT = 7.5; const ZOOM_COEFFICIENT = 0.5;
// If we have moved only this much we can zoom // If we have moved only this much we can zoom
const ZOOM_DISTANCE = 10; const ZOOM_DISTANCE = 10;
@ -115,7 +116,9 @@ export default class ImageView extends React.Component<IProps, IState> {
private onWheel = (ev: WheelEvent) => { private onWheel = (ev: WheelEvent) => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
const newZoom = this.state.zoom - (ev.deltaY * ZOOM_COEFFICIENT);
const {deltaY} = normalizeWheelEvent(ev);
const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT);
if (newZoom <= MIN_ZOOM) { if (newZoom <= MIN_ZOOM) {
this.setState({ this.setState({

View file

@ -25,6 +25,7 @@ export default class TextWithTooltip extends React.Component {
class: PropTypes.string, class: PropTypes.string,
tooltipClass: PropTypes.string, tooltipClass: PropTypes.string,
tooltip: PropTypes.node.isRequired, tooltip: PropTypes.node.isRequired,
tooltipProps: PropTypes.object,
}; };
constructor() { constructor() {
@ -46,15 +47,17 @@ export default class TextWithTooltip extends React.Component {
render() { render() {
const Tooltip = sdk.getComponent("elements.Tooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
const {class: className, children, tooltip, tooltipClass, ...props} = this.props; const {class: className, children, tooltip, tooltipClass, tooltipProps, ...props} = this.props;
return ( return (
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}> <span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
{children} {children}
{this.state.hover && <Tooltip {this.state.hover && <Tooltip
{...tooltipProps}
label={tooltip} label={tooltip}
tooltipClassName={tooltipClass} tooltipClassName={tooltipClass}
className={"mx_TextWithTooltip_tooltip"} /> } className={"mx_TextWithTooltip_tooltip"}
/> }
</span> </span>
); );
} }

View file

@ -548,6 +548,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
} }
public render() { public render() {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
let explorePrompt: JSX.Element; let explorePrompt: JSX.Element;
if (!this.props.isMinimized) { if (!this.props.isMinimized) {
if (this.state.isNameFiltering) { if (this.state.isNameFiltering) {
@ -568,21 +571,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") } { this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
</AccessibleButton> </AccessibleButton>
</div>; </div>;
} else if (this.props.activeSpace) { } else if (
this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join"
) {
explorePrompt = <div className="mx_RoomList_explorePrompt"> explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Quick actions") }</div> <div>{ _t("Quick actions") }</div>
{ this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton { this.props.activeSpace.canInvite(userId) && <AccessibleButton
className="mx_RoomList_explorePrompt_spaceInvite" className="mx_RoomList_explorePrompt_spaceInvite"
onClick={this.onSpaceInviteClick} onClick={this.onSpaceInviteClick}
> >
{_t("Invite people")} {_t("Invite people")}
</AccessibleButton> } </AccessibleButton> }
<AccessibleButton { this.props.activeSpace.getMyMembership() === "join" && <AccessibleButton
className="mx_RoomList_explorePrompt_spaceExplore" className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore} onClick={this.onExplore}
> >
{_t("Explore rooms")} {_t("Explore rooms")}
</AccessibleButton> </AccessibleButton> }
</div>; </div>;
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) { } else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
const unfilteredLists = RoomListStore.instance.unfilteredLists const unfilteredLists = RoomListStore.instance.unfilteredLists

View file

@ -53,9 +53,38 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
await this.state.recorder.stop(); await this.state.recorder.stop();
const mxc = await this.state.recorder.upload(); const mxc = await this.state.recorder.upload();
MatrixClientPeg.get().sendMessage(this.props.room.roomId, { MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
body: "Voice message", "body": "Voice message",
msgtype: "org.matrix.msc2516.voice", "msgtype": "org.matrix.msc2516.voice",
//"msgtype": MsgType.Audio,
"url": mxc,
"info": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
// MSC1767 experiment
"org.matrix.msc1767.text": "Voice message",
"org.matrix.msc1767.file": {
url: mxc, url: mxc,
name: "Voice message.ogg",
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
"org.matrix.msc1767.audio": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
// TODO: @@ TravisR: Waveform? (MSC1767 decision)
},
"org.matrix.experimental.msc2516.voice": { // MSC2516+MSC1767 experiment
duration: Math.round(this.state.recorder.durationSeconds * 1000),
// Events can't have floats, so we try to maintain resolution by using 1024
// as a maximum value. The waveform contains values between zero and 1, so this
// should come out largely sane.
//
// We're expecting about one data point per second of audio.
waveform: this.state.recorder.finalWaveform.map(v => Math.round(v * 1024)),
},
}); });
await VoiceRecordingStore.instance.disposeRecording(); await VoiceRecordingStore.instance.disposeRecording();
this.setState({recorder: null}); this.setState({recorder: null});

View file

@ -25,7 +25,12 @@ import SpaceCreateMenu from "./SpaceCreateMenu";
import {SpaceItem} from "./SpaceTreeLevel"; import {SpaceItem} from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {useEventEmitter} from "../../../hooks/useEventEmitter";
import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore"; import SpaceStore, {
HOME_SPACE,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
} from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState"; import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
import NotificationBadge from "../rooms/NotificationBadge"; import NotificationBadge from "../rooms/NotificationBadge";
@ -105,19 +110,21 @@ const SpaceButton: React.FC<IButtonProps> = ({
</li>; </li>;
} }
const useSpaces = (): [Room[], Room | null] => { const useSpaces = (): [Room[], Room[], Room | null] => {
const [invites, setInvites] = useState<Room[]>(SpaceStore.instance.invitedSpaces);
useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites);
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces); const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces); useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace); const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace); useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
return [spaces, activeSpace]; return [invites, spaces, activeSpace];
}; };
const SpacePanel = () => { const SpacePanel = () => {
// We don't need the handle as we position the menu in a constant location // We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>(); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
const [spaces, activeSpace] = useSpaces(); const [invites, spaces, activeSpace] = useSpaces();
const [isPanelCollapsed, setPanelCollapsed] = useState(true); const [isPanelCollapsed, setPanelCollapsed] = useState(true);
const newClasses = classNames("mx_SpaceButton_new", { const newClasses = classNames("mx_SpaceButton_new", {
@ -209,6 +216,13 @@ const SpacePanel = () => {
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)} notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
isNarrow={isPanelCollapsed} isNarrow={isPanelCollapsed}
/> />
{ invites.map(s => <SpaceItem
key={s.roomId}
space={s}
activeSpaces={activeSpaces}
isPanelCollapsed={isPanelCollapsed}
onExpand={() => setPanelCollapsed(false)}
/>) }
{ spaces.map(s => <SpaceItem { spaces.map(s => <SpaceItem
key={s.roomId} key={s.roomId}
space={s} space={s}

View file

@ -45,6 +45,8 @@ import RoomViewStore from "../../../stores/RoomViewStore";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {EventType} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
import {NotificationColor} from "../../../stores/notifications/NotificationColor";
interface IItemProps { interface IItemProps {
space?: Room; space?: Room;
@ -83,6 +85,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
} }
private onContextMenu = (ev: React.MouseEvent) => { private onContextMenu = (ev: React.MouseEvent) => {
if (this.props.space.getMyMembership() !== "join") return;
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.setState({ this.setState({
@ -185,6 +188,8 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
}; };
private renderContextMenu(): React.ReactElement { private renderContextMenu(): React.ReactElement {
if (this.props.space.getMyMembership() !== "join") return null;
let contextMenu = null; let contextMenu = null;
if (this.state.contextMenuPosition) { if (this.state.contextMenuPosition) {
const userId = this.context.getUserId(); const userId = this.context.getUserId();
@ -300,7 +305,9 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
mx_SpaceButton_narrow: isNarrow, mx_SpaceButton_narrow: isNarrow,
}); });
const notificationState = SpaceStore.instance.getNotificationState(space.roomId); const notificationState = space.getMyMembership() === "invite"
? StaticNotificationState.forSymbol("!", NotificationColor.Red)
: SpaceStore.instance.getNotificationState(space.roomId);
let childItems; let childItems;
if (childSpaces && !collapsed) { if (childSpaces && !collapsed) {

View file

@ -1916,7 +1916,13 @@
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.", "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
"collapse": "collapse", "collapse": "collapse",
"expand": "expand", "expand": "expand",
"View all %(count)s members|other": "View all %(count)s members",
"View all %(count)s members|one": "View 1 member",
"Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s",
"%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s members including %(commaSeparatedMembers)s",
"%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s",
"%(count)s people you know have already joined|other": "%(count)s people you know have already joined", "%(count)s people you know have already joined|other": "%(count)s people you know have already joined",
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
"Rotate Right": "Rotate Right", "Rotate Right": "Rotate Right",
"Rotate Left": "Rotate Left", "Rotate Left": "Rotate Left",
"Zoom out": "Zoom out", "Zoom out": "Zoom out",

View file

@ -46,16 +46,13 @@ export const HOME_SPACE = Symbol("home-space");
export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
// Space Room ID/HOME_SPACE will be emitted when a Space's children change // Space Room ID/HOME_SPACE will be emitted when a Space's children change
const MAX_SUGGESTED_ROOMS = 20; const MAX_SUGGESTED_ROOMS = 20;
const getLastViewedRoomsStorageKey = (space?: Room) => { const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "home_space"}`;
const lastViewRooms = "mx_last_viewed_rooms";
const homeSpace = "home_space";
return `${lastViewRooms}_${space?.roomId || homeSpace}`;
}
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
return arr.reduce((result, room: Room) => { return arr.reduce((result, room: Room) => {
@ -97,6 +94,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// The space currently selected in the Space Panel - if null then `Home` is selected // The space currently selected in the Space Panel - if null then `Home` is selected
private _activeSpace?: Room = null; private _activeSpace?: Room = null;
private _suggestedRooms: ISpaceSummaryRoom[] = []; private _suggestedRooms: ISpaceSummaryRoom[] = [];
private _invitedSpaces = new Set<Room>();
public get invitedSpaces(): Room[] {
return Array.from(this._invitedSpaces);
}
public get spacePanelSpaces(): Room[] { public get spacePanelSpaces(): Room[] {
return this.rootSpaces; return this.rootSpaces;
@ -110,17 +112,23 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return this._suggestedRooms; return this._suggestedRooms;
} }
public async setActiveSpace(space: Room | null) { public async setActiveSpace(space: Room | null, contextSwitch = true) {
if (space === this.activeSpace) return; if (space === this.activeSpace) return;
this._activeSpace = space; this._activeSpace = space;
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []);
if (contextSwitch) {
// view last selected room from space // view last selected room from space
const roomId = window.localStorage.getItem(getLastViewedRoomsStorageKey(this.activeSpace)); const roomId = window.localStorage.getItem(getSpaceContextKey(this.activeSpace));
if (roomId && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join") { // if the space being selected is an invite then always view that invite
// else if the last viewed room in this space is joined then view that
// else view space home or home depending on what is being clicked on
if (space?.getMyMembership !== "invite" &&
this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join"
) {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "view_room", action: "view_room",
room_id: roomId, room_id: roomId,
@ -137,6 +145,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
action: "view_home_page", action: "view_home_page",
}); });
} }
}
// persist space selected // persist space selected
if (space) { if (space) {
@ -216,25 +225,27 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return sortBy(parents, r => r.roomId)?.[0] || null; return sortBy(parents, r => r.roomId)?.[0] || null;
} }
public getSpaces = () => {
return this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "join");
};
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => { public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
}; };
private rebuild = throttle(() => { private rebuild = throttle(() => {
// get all most-upgraded rooms & spaces except spaces which have been left (historical) const [visibleSpaces, visibleRooms] = partitionSpacesAndRooms(this.matrixClient.getVisibleRooms());
const visibleRooms = this.matrixClient.getVisibleRooms().filter(r => { const [joinedSpaces, invitedSpaces] = visibleSpaces.reduce((arr, s) => {
return !r.isSpaceRoom() || r.getMyMembership() === "join"; if (s.getMyMembership() === "join") {
}); arr[0].push(s);
} else if (s.getMyMembership() === "invite") {
arr[1].push(s);
}
return arr;
}, [[], []]);
const unseenChildren = new Set<Room>(visibleRooms); // exclude invited spaces from unseenChildren as they will be forcibly shown at the top level of the treeview
const unseenChildren = new Set<Room>([...visibleRooms, ...joinedSpaces]);
const backrefs = new EnhancedMap<string, Set<string>>(); const backrefs = new EnhancedMap<string, Set<string>>();
// Sort spaces by room ID to force the cycle breaking to be deterministic // Sort spaces by room ID to force the cycle breaking to be deterministic
const spaces = sortBy(visibleRooms.filter(r => r.isSpaceRoom()), space => space.roomId); const spaces = sortBy(joinedSpaces, space => space.roomId);
// TODO handle cleaning up links when a Space is removed // TODO handle cleaning up links when a Space is removed
spaces.forEach(space => { spaces.forEach(space => {
@ -298,6 +309,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.onRoomsUpdate(); // TODO only do this if a change has happened this.onRoomsUpdate(); // TODO only do this if a change has happened
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
this._invitedSpaces = new Set(invitedSpaces);
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
}, 100, {trailing: true, leading: true}); }, 100, {trailing: true, leading: true});
onSpaceUpdate = () => { onSpaceUpdate = () => {
@ -305,6 +320,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
private showInHomeSpace = (room: Room) => { private showInHomeSpace = (room: Room) => {
if (room.isSpaceRoom()) return false;
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
@ -335,8 +351,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const oldFilteredRooms = this.spaceFilteredRooms; const oldFilteredRooms = this.spaceFilteredRooms;
this.spaceFilteredRooms = new Map(); this.spaceFilteredRooms = new Map();
// put all invites (rooms & spaces) in the Home Space // put all room invites in the Home Space
const invites = this.matrixClient.getRooms().filter(r => r.getMyMembership() === "invite"); const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId))); this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
visibleRooms.forEach(room => { visibleRooms.forEach(room => {
@ -389,13 +405,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.spaceFilteredRooms.forEach((roomIds, s) => { this.spaceFilteredRooms.forEach((roomIds, s) => {
// Update NotificationStates // Update NotificationStates
const rooms = this.matrixClient.getRooms().filter(room => roomIds.has(room.roomId)); this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => roomIds.has(room.roomId)));
this.getNotificationState(s)?.setRooms(rooms);
}); });
}, 100, {trailing: true, leading: true}); }, 100, {trailing: true, leading: true});
private onRoom = (room: Room) => { private onRoom = (room: Room, membership?: string, oldMembership?: string) => {
if (room?.isSpaceRoom()) { if ((membership || room.getMyMembership()) === "invite") {
this._invitedSpaces.add(room);
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
} else if (oldMembership === "invite") {
this._invitedSpaces.delete(room);
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
} else if (room?.isSpaceRoom()) {
this.onSpaceUpdate(); this.onSpaceUpdate();
this.emit(room.roomId); this.emit(room.roomId);
} else { } else {
@ -517,23 +538,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// Don't auto-switch rooms when reacting to a context-switch // Don't auto-switch rooms when reacting to a context-switch
// as this is not helpful and can create loops of rooms/space switching // as this is not helpful and can create loops of rooms/space switching
if (payload.context_switch) break; if (!room || payload.context_switch) break;
// persist last viewed room from a space // persist last viewed room from a space
// Don't save if the room is a space room. This would cause a problem:
// When switching to a space home, we first view that room and
// only after that we switch to that space. This causes us to
// save the space home to be the last viewed room in the home
// space.
if (room && !room.isSpaceRoom()) {
window.localStorage.setItem(getLastViewedRoomsStorageKey(this.activeSpace), payload.room_id);
}
if (room?.getMyMembership() === "join") {
if (room.isSpaceRoom()) { if (room.isSpaceRoom()) {
this.setActiveSpace(room); this.setActiveSpace(room);
} else if (!this.spaceFilteredRooms.get(this._activeSpace?.roomId || HOME_SPACE).has(room.roomId)) { } else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(room.roomId)) {
// TODO maybe reverse these first 2 clauses once space panel active is fixed // TODO maybe reverse these first 2 clauses once space panel active is fixed
let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId)); let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId));
if (!parent) { if (!parent) {
@ -543,11 +554,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const parents = Array.from(this.parentMap.get(room.roomId) || []); const parents = Array.from(this.parentMap.get(room.roomId) || []);
parent = parents.find(p => this.matrixClient.getRoom(p)); parent = parents.find(p => this.matrixClient.getRoom(p));
} }
if (parent) { // don't trigger a context switch when we are switching a space to match the chosen room
this.setActiveSpace(parent); this.setActiveSpace(parent || null, false);
}
}
} }
// Persist last viewed room from a space
// we don't await setActiveSpace above as we only care about this.activeSpace being up to date
// synchronously for the below code - everything else can and should be async.
window.localStorage.setItem(getSpaceContextKey(this.activeSpace), payload.room_id);
break; break;
} }
case "after_leave_room": case "after_leave_room":

View file

@ -599,11 +599,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
private getPlausibleRooms(): Room[] { private getPlausibleRooms(): Room[] {
if (!this.matrixClient) return []; if (!this.matrixClient) return [];
let rooms = [ let rooms = this.matrixClient.getVisibleRooms().filter(r => VisibilityProvider.instance.isRoomVisible(r));
...this.matrixClient.getVisibleRooms(),
// also show space invites in the room list
...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"),
].filter(r => VisibilityProvider.instance.isRoomVisible(r));
if (this.prefilterConditions.length > 0) { if (this.prefilterConditions.length > 0) {
rooms = rooms.filter(r => { rooms = rooms.filter(r => {

50
src/utils/Mouse.ts Normal file
View file

@ -0,0 +1,50 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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.
*/
/**
* Different browsers use different deltaModes. This causes different behaviour.
* To avoid that we use this function to convert any event to pixels.
* @param {WheelEvent} event to normalize
* @returns {WheelEvent} normalized event event
*/
export function normalizeWheelEvent(event: WheelEvent): WheelEvent {
const LINE_HEIGHT = 18;
let deltaX;
let deltaY;
let deltaZ;
if (event.deltaMode === 1) { // Units are lines
deltaX = (event.deltaX * LINE_HEIGHT);
deltaY = (event.deltaY * LINE_HEIGHT);
deltaZ = (event.deltaZ * LINE_HEIGHT);
} else {
deltaX = event.deltaX;
deltaY = event.deltaY;
deltaZ = event.deltaZ;
}
return new WheelEvent(
"syntheticWheel",
{
deltaMode: 0,
deltaY: deltaY,
deltaX: deltaX,
deltaZ: deltaZ,
...event,
},
);
}

View file

@ -54,7 +54,7 @@ export function arraySeed<T>(val: T, length: number): T[] {
* @param a The array to clone. Must be defined. * @param a The array to clone. Must be defined.
* @returns A copy of the array. * @returns A copy of the array.
*/ */
export function arrayFastClone(a: any[]): any[] { export function arrayFastClone<T>(a: T[]): T[] {
return a.slice(0, a.length); return a.slice(0, a.length);
} }

View file

@ -0,0 +1,67 @@
/*
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 {IAmplitudePayload, ITimingPayload, PayloadEvent, WORKLET_NAME} from "./consts";
import {percentageOf} from "../utils/numbers";
// from AudioWorkletGlobalScope: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletGlobalScope
declare const currentTime: number;
// declare const currentFrame: number;
// declare const sampleRate: number;
class MxVoiceWorklet extends AudioWorkletProcessor {
private nextAmplitudeSecond = 0;
process(inputs, outputs, parameters) {
// We only fire amplitude updates once a second to avoid flooding the recording instance
// with useless data. Much of the data would end up discarded, so we ratelimit ourselves
// here.
const currentSecond = Math.round(currentTime);
if (currentSecond === this.nextAmplitudeSecond) {
// We're expecting exactly one mono input source, so just grab the very first frame of
// samples for the analysis.
const monoChan = inputs[0][0];
// The amplitude of the frame's samples is effectively the loudness of the frame. This
// translates into a bar which can be rendered as part of the whole recording clip's
// waveform.
//
// We translate the amplitude down to 0-1 for sanity's sake.
const minVal = Math.min(...monoChan);
const maxVal = Math.max(...monoChan);
const amplitude = percentageOf(maxVal, -1, 1) - percentageOf(minVal, -1, 1);
this.port.postMessage(<IAmplitudePayload>{
ev: PayloadEvent.AmplitudeMark,
amplitude: amplitude,
forSecond: currentSecond,
});
this.nextAmplitudeSecond++;
}
// We mostly use this worklet to fire regular clock updates through to components
this.port.postMessage(<ITimingPayload>{ev: PayloadEvent.Timekeep, timeSeconds: currentTime});
// We're supposed to return false when we're "done" with the audio clip, but seeing as
// we are acting as a passive processor we are never truly "done". The browser will clean
// us up when it is done with us.
return true;
}
}
registerProcessor(WORKLET_NAME, MxVoiceWorklet);
export default null; // to appease module loaders (we never use the export)

View file

@ -23,6 +23,8 @@ import {clamp} from "../utils/numbers";
import EventEmitter from "events"; import EventEmitter from "events";
import {IDestroyable} from "../utils/IDestroyable"; import {IDestroyable} from "../utils/IDestroyable";
import {Singleflight} from "../utils/Singleflight"; import {Singleflight} from "../utils/Singleflight";
import {PayloadEvent, WORKLET_NAME} from "./consts";
import {arrayFastClone} from "../utils/arrays";
const CHANNELS = 1; // stereo isn't important const CHANNELS = 1; // stereo isn't important
const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
@ -49,16 +51,34 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
private recorderSource: MediaStreamAudioSourceNode; private recorderSource: MediaStreamAudioSourceNode;
private recorderStream: MediaStream; private recorderStream: MediaStream;
private recorderFFT: AnalyserNode; private recorderFFT: AnalyserNode;
private recorderProcessor: ScriptProcessorNode; private recorderWorklet: AudioWorkletNode;
private buffer = new Uint8Array(0); private buffer = new Uint8Array(0);
private mxc: string; private mxc: string;
private recording = false; private recording = false;
private observable: SimpleObservable<IRecordingUpdate>; private observable: SimpleObservable<IRecordingUpdate>;
private amplitudes: number[] = []; // at each second mark, generated
public constructor(private client: MatrixClient) { public constructor(private client: MatrixClient) {
super(); super();
} }
public get finalWaveform(): number[] {
return arrayFastClone(this.amplitudes);
}
public get contentType(): string {
return "audio/ogg";
}
public get contentLength(): number {
return this.buffer.length;
}
public get durationSeconds(): number {
if (!this.recorder) throw new Error("Duration not available without a recording");
return this.recorderContext.currentTime;
}
private async makeRecorder() { private async makeRecorder() {
this.recorderStream = await navigator.mediaDevices.getUserMedia({ this.recorderStream = await navigator.mediaDevices.getUserMedia({
audio: { audio: {
@ -80,18 +100,34 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
// it makes the time domain less than helpful. // it makes the time domain less than helpful.
this.recorderFFT.fftSize = 64; this.recorderFFT.fftSize = 64;
// We use an audio processor to get accurate timing information. // Set up our worklet. We use this for timing information and waveform analysis: the
// The size of the audio buffer largely decides how quickly we push timing/waveform data // web audio API prefers this be done async to avoid holding the main thread with math.
// out of this class. Smaller buffers mean we update more frequently as we can't hold as const mxRecorderWorkletPath = document.body.dataset.vectorRecorderWorkletScript;
// many bytes. Larger buffers mean slower updates. For scale, 1024 gives us about 30Hz of if (!mxRecorderWorkletPath) {
// updates and 2048 gives us about 20Hz. We use 1024 to get as close to perceived realtime throw new Error("Unable to create recorder: no worklet script registered");
// as possible. Must be a power of 2. }
this.recorderProcessor = this.recorderContext.createScriptProcessor(1024, CHANNELS, CHANNELS); await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath);
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
// Connect our inputs and outputs // Connect our inputs and outputs
this.recorderSource.connect(this.recorderFFT); this.recorderSource.connect(this.recorderFFT);
this.recorderSource.connect(this.recorderProcessor); this.recorderSource.connect(this.recorderWorklet);
this.recorderProcessor.connect(this.recorderContext.destination); this.recorderWorklet.connect(this.recorderContext.destination);
// Dev note: we can't use `addEventListener` for some reason. It just doesn't work.
this.recorderWorklet.port.onmessage = (ev) => {
switch (ev.data['ev']) {
case PayloadEvent.Timekeep:
this.processAudioUpdate(ev.data['timeSeconds']);
break;
case PayloadEvent.AmplitudeMark:
// Sanity check to make sure we're adding about one sample per second
if (ev.data['forSecond'] === this.amplitudes.length) {
this.amplitudes.push(ev.data['amplitude']);
}
break;
}
};
this.recorder = new Recorder({ this.recorder = new Recorder({
encoderPath, // magic from webpack encoderPath, // magic from webpack
@ -138,7 +174,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
return this.mxc; return this.mxc;
} }
private processAudioUpdate = (ev: AudioProcessingEvent) => { private processAudioUpdate = (timeSeconds: number) => {
if (!this.recording) return; if (!this.recording) return;
// The time domain is the input to the FFT, which means we use an array of the same // The time domain is the input to the FFT, which means we use an array of the same
@ -162,12 +198,12 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
this.observable.update({ this.observable.update({
waveform: translatedData, waveform: translatedData,
timeSeconds: ev.playbackTime, timeSeconds: timeSeconds,
}); });
// Now that we've updated the data/waveform, let's do a time check. We don't want to // Now that we've updated the data/waveform, let's do a time check. We don't want to
// go horribly over the limit. We also emit a warning state if needed. // go horribly over the limit. We also emit a warning state if needed.
const secondsLeft = TARGET_MAX_LENGTH - ev.playbackTime; const secondsLeft = TARGET_MAX_LENGTH - timeSeconds;
if (secondsLeft <= 0) { if (secondsLeft <= 0) {
// noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping // noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping
this.stop(); this.stop();
@ -191,7 +227,6 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
} }
this.observable = new SimpleObservable<IRecordingUpdate>(); this.observable = new SimpleObservable<IRecordingUpdate>();
await this.makeRecorder(); await this.makeRecorder();
this.recorderProcessor.addEventListener("audioprocess", this.processAudioUpdate);
await this.recorder.start(); await this.recorder.start();
this.recording = true; this.recording = true;
this.emit(RecordingState.Started); this.emit(RecordingState.Started);
@ -205,6 +240,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
// Disconnect the source early to start shutting down resources // Disconnect the source early to start shutting down resources
this.recorderSource.disconnect(); this.recorderSource.disconnect();
this.recorderWorklet.disconnect();
await this.recorder.stop(); await this.recorder.stop();
// close the context after the recorder so the recorder doesn't try to // close the context after the recorder so the recorder doesn't try to
@ -216,7 +252,6 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
// Finally do our post-processing and clean up // Finally do our post-processing and clean up
this.recording = false; this.recording = false;
this.recorderProcessor.removeEventListener("audioprocess", this.processAudioUpdate);
await this.recorder.close(); await this.recorder.close();
this.emit(RecordingState.Ended); this.emit(RecordingState.Ended);
@ -240,7 +275,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
this.emit(RecordingState.Uploading); this.emit(RecordingState.Uploading);
this.mxc = await this.client.uploadContent(new Blob([this.buffer], { this.mxc = await this.client.uploadContent(new Blob([this.buffer], {
type: "audio/ogg", type: this.contentType,
}), { }), {
onlyContentUri: false, // to stop the warnings in the console onlyContentUri: false, // to stop the warnings in the console
}).then(r => r['content_uri']); }).then(r => r['content_uri']);

37
src/voice/consts.ts Normal file
View file

@ -0,0 +1,37 @@
/*
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.
*/
export const WORKLET_NAME = "mx-voice-worklet";
export enum PayloadEvent {
Timekeep = "timekeep",
AmplitudeMark = "amplitude_mark",
}
export interface IPayload {
ev: PayloadEvent;
}
export interface ITimingPayload extends IPayload {
ev: PayloadEvent.Timekeep;
timeSeconds: number;
}
export interface IAmplitudePayload extends IPayload {
ev: PayloadEvent.AmplitudeMark;
forSecond: number;
amplitude: number;
}