Merge branch 'develop' into travis/msc2873
This commit is contained in:
commit
89933d779c
41 changed files with 913 additions and 663 deletions
|
@ -167,7 +167,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
|
|||
const onKeyDownHandler = useCallback((ev) => {
|
||||
let handled = false;
|
||||
// Don't interfere with input default keydown behaviour
|
||||
if (handleHomeEnd && ev.target.tagName !== "INPUT") {
|
||||
if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
||||
// check if we actually have any items
|
||||
switch (ev.key) {
|
||||
case Key.HOME:
|
||||
|
|
|
@ -222,10 +222,12 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
// don't let keyboard handling escape the context menu
|
||||
ev.stopPropagation();
|
||||
|
||||
if (!this.props.managed) {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
this.props.onFinished();
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
return;
|
||||
|
@ -258,7 +260,6 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
|
||||
if (handled) {
|
||||
// consume all other keys in context menu
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz
|
|||
import DMRoomMap from "../../utils/DMRoomMap";
|
||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import defaultDispatcher from '../../dispatcher/dispatcher';
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
@ -471,6 +472,10 @@ export default class MessagePanel extends React.Component {
|
|||
return {nextEvent, nextTile};
|
||||
}
|
||||
|
||||
get _roomHasPendingEdit() {
|
||||
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
||||
}
|
||||
|
||||
_getEventTiles() {
|
||||
this.eventNodes = {};
|
||||
|
||||
|
@ -559,6 +564,13 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.props.editState && this._roomHasPendingEdit) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "edit_event",
|
||||
event: this.props.room.findEventById(this._roomHasPendingEdit),
|
||||
});
|
||||
}
|
||||
|
||||
if (grouper) {
|
||||
ret.push(...grouper.getTiles());
|
||||
}
|
||||
|
@ -574,7 +586,6 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
const isEditing = this.props.editState &&
|
||||
this.props.editState.getEvent().getId() === mxEv.getId();
|
||||
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||
// as 'today' for the date separators.
|
||||
let ts1 = mxEv.getTs();
|
||||
|
|
|
@ -52,8 +52,6 @@ import {useStateToggle} from "../../hooks/useStateToggle";
|
|||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import FacePile from "../views/elements/FacePile";
|
||||
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||
import {sleep} from "../../utils/promise";
|
||||
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
|
||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
|
@ -78,6 +76,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
createdRooms?: boolean; // internal state for the creation wizard
|
||||
showRightPanel: boolean;
|
||||
myMembership: string;
|
||||
}
|
||||
|
@ -461,7 +460,8 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||
setError("");
|
||||
setBusy(true);
|
||||
try {
|
||||
await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => {
|
||||
const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean);
|
||||
await Promise.all(filteredRoomNames.map(name => {
|
||||
return createRoom({
|
||||
createOpts: {
|
||||
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
|
||||
|
@ -474,7 +474,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||
parentSpace: space,
|
||||
});
|
||||
}));
|
||||
onFinished();
|
||||
onFinished(filteredRoomNames.length > 0);
|
||||
} catch (e) {
|
||||
console.error("Failed to create initial space rooms", e);
|
||||
setError(_t("Failed to create initial space rooms"));
|
||||
|
@ -484,7 +484,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||
|
||||
let onClick = (ev) => {
|
||||
ev.preventDefault();
|
||||
onFinished();
|
||||
onFinished(false);
|
||||
};
|
||||
let buttonLabel = _t("Skip for now");
|
||||
if (roomNames.some(name => name.trim())) {
|
||||
|
@ -517,39 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||
};
|
||||
|
||||
const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
let onClick = onFinished;
|
||||
let buttonLabel = _t("Skip for now");
|
||||
if (selectedToAdd.size > 0) {
|
||||
onClick = async () => {
|
||||
setBusy(true);
|
||||
|
||||
for (const room of selectedToAdd) {
|
||||
const via = calculateRoomVia(room);
|
||||
try {
|
||||
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
|
||||
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||
await sleep(e.data.retry_after_ms);
|
||||
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to add rooms to space", e);
|
||||
setError(_t("Failed to add rooms to space"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
setBusy(false);
|
||||
};
|
||||
buttonLabel = busy ? _t("Adding...") : _t("Add");
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h1>{ _t("What do you want to organise?") }</h1>
|
||||
<div className="mx_SpaceRoomView_description">
|
||||
|
@ -557,35 +524,24 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
|||
"no one will be informed. You can add more later.") }
|
||||
</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<AddExistingToSpace
|
||||
space={space}
|
||||
selected={selectedToAdd}
|
||||
onChange={(checked, room) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(room);
|
||||
} else {
|
||||
selectedToAdd.delete(room);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
}}
|
||||
emptySelectionButton={
|
||||
<AccessibleButton kind="primary" onClick={onFinished}>
|
||||
{ _t("Skip for now") }
|
||||
</AccessibleButton>
|
||||
}
|
||||
onFinished={onFinished}
|
||||
/>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={busy}
|
||||
onClick={onClick}
|
||||
>
|
||||
{ buttonLabel }
|
||||
</AccessibleButton>
|
||||
|
||||
</div>
|
||||
<SpaceFeedbackPrompt />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => {
|
||||
const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRooms }) => {
|
||||
return <div className="mx_SpaceRoomView_publicShare">
|
||||
<h1>{ _t("Share %(name)s", {
|
||||
name: justCreatedOpts?.createOpts?.name || space.name,
|
||||
|
@ -598,7 +554,7 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => {
|
|||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<AccessibleButton kind="primary" onClick={onFinished}>
|
||||
{ _t("Go to my first room") }
|
||||
{ createdRooms ? _t("Go to my first room") : _t("Go to my space") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<SpaceFeedbackPrompt />
|
||||
|
@ -891,13 +847,14 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
_t("Let's create a room for each of them.") + "\n" +
|
||||
_t("You can add more later too, including already existing ones.")
|
||||
}
|
||||
onFinished={() => this.setState({ phase: Phase.PublicShare })}
|
||||
onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.PublicShare, createdRooms })}
|
||||
/>;
|
||||
case Phase.PublicShare:
|
||||
return <SpaceSetupPublicShare
|
||||
justCreatedOpts={this.props.justCreatedOpts}
|
||||
space={this.props.space}
|
||||
onFinished={this.goToFirstRoom}
|
||||
createdRooms={this.state.createdRooms}
|
||||
/>;
|
||||
|
||||
case Phase.PrivateScope:
|
||||
|
@ -919,7 +876,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
title={_t("What projects are you working on?")}
|
||||
description={_t("We'll create rooms for each of them. " +
|
||||
"You can add more later too, including already existing ones.")}
|
||||
onFinished={() => this.setState({ phase: Phase.Landing })}
|
||||
onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.Landing, createdRooms })}
|
||||
/>;
|
||||
case Phase.PrivateExistingRooms:
|
||||
return <SpaceAddExistingRooms
|
||||
|
|
|
@ -20,7 +20,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { TagID } from '../../../stores/room-list/models';
|
||||
import RoomAvatar from "./RoomAvatar";
|
||||
import NotificationBadge from '../rooms/NotificationBadge';
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
|
@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|||
interface IProps {
|
||||
room: Room;
|
||||
avatarSize: number;
|
||||
tag: TagID;
|
||||
displayBadge?: boolean;
|
||||
forceCount?: boolean;
|
||||
oobData?: object;
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useMemo, useState} from "react";
|
||||
import React, {ReactNode, useContext, useMemo, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
@ -37,6 +37,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|||
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import ProgressBar from "../elements/ProgressBar";
|
||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
@ -46,7 +47,10 @@ interface IProps extends IDialogProps {
|
|||
|
||||
const Entry = ({ room, checked, onChange }) => {
|
||||
return <label className="mx_AddExistingToSpace_entry">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
{ room?.isSpaceRoom()
|
||||
? <RoomAvatar room={room} height={32} width={32} />
|
||||
: <DecoratedRoomAvatar room={room} avatarSize={32} />
|
||||
}
|
||||
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
|
||||
<StyledCheckbox
|
||||
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
||||
|
@ -58,14 +62,23 @@ const Entry = ({ room, checked, onChange }) => {
|
|||
|
||||
interface IAddExistingToSpaceProps {
|
||||
space: Room;
|
||||
selected: Set<Room>;
|
||||
onChange(checked: boolean, room: Room): void;
|
||||
footerPrompt?: ReactNode;
|
||||
emptySelectionButton?: ReactNode;
|
||||
onFinished(added: boolean): void;
|
||||
}
|
||||
|
||||
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => {
|
||||
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||
space,
|
||||
footerPrompt,
|
||||
emptySelectionButton,
|
||||
onFinished,
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
|
||||
|
||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||
const [progress, setProgress] = useState<number>(null);
|
||||
const [error, setError] = useState<Error>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
|
@ -93,120 +106,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
|
|||
return arr;
|
||||
}, [[], [], []]);
|
||||
|
||||
return <div className="mx_AddExistingToSpace">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Filter your rooms and spaces") }
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
{ rooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selected.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : undefined }
|
||||
|
||||
{ spaces.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
<div className="mx_AddExistingToSpace_section_experimental">
|
||||
<div>{ _t("Feeling experimental?") }</div>
|
||||
<div>{ _t("You can add existing spaces to a space.") }</div>
|
||||
</div>
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selected.has(space)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, space);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ dms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Direct Messages") }</h3>
|
||||
{ dms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selected.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||
|
||||
const [progress, setProgress] = useState<number>(null);
|
||||
const [error, setError] = useState<Error>(null);
|
||||
|
||||
let spaceOptionSection;
|
||||
if (existingSubspaces.length > 0) {
|
||||
const options = [space, ...existingSubspaces].map((space) => {
|
||||
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
||||
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
||||
});
|
||||
return <div key={space.roomId} className={classes}>
|
||||
<RoomAvatar room={space} width={24} height={24} />
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
});
|
||||
|
||||
spaceOptionSection = (
|
||||
<Dropdown
|
||||
id="mx_SpaceSelectDropdown"
|
||||
onOptionChange={(key: string) => {
|
||||
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
|
||||
}}
|
||||
value={selectedSpace.roomId}
|
||||
label={_t("Space selection")}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>
|
||||
);
|
||||
} else {
|
||||
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
}
|
||||
|
||||
const title = <React.Fragment>
|
||||
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
||||
<div>
|
||||
<h1>{ _t("Add existing rooms") }</h1>
|
||||
{ spaceOptionSection }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
const addRooms = async () => {
|
||||
setError(null);
|
||||
setProgress(0);
|
||||
|
@ -269,20 +168,145 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
</div>
|
||||
</span>;
|
||||
} else {
|
||||
let button = emptySelectionButton;
|
||||
if (!button || selectedToAdd.size > 0) {
|
||||
button = <AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
footer = <>
|
||||
<span>
|
||||
<div>{ _t("Want to add a new room instead?") }</div>
|
||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
{ footerPrompt }
|
||||
</span>
|
||||
|
||||
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
{ button }
|
||||
</>;
|
||||
}
|
||||
|
||||
const onChange = !busy && !error ? (checked, room) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(room);
|
||||
} else {
|
||||
selectedToAdd.delete(room);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
} : null;
|
||||
|
||||
return <div className="mx_AddExistingToSpace">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Filter your rooms and spaces") }
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
{ rooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : undefined }
|
||||
|
||||
{ spaces.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
<div className="mx_AddExistingToSpace_section_experimental">
|
||||
<div>{ _t("Feeling experimental?") }</div>
|
||||
<div>{ _t("You can add existing spaces to a space.") }</div>
|
||||
</div>
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selectedToAdd.has(space)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, space);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ dms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Direct Messages") }</h3>
|
||||
{ dms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
|
||||
<div className="mx_AddExistingToSpace_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
|
||||
let spaceOptionSection;
|
||||
if (existingSubspaces.length > 0) {
|
||||
const options = [space, ...existingSubspaces].map((space) => {
|
||||
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
||||
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
||||
});
|
||||
return <div key={space.roomId} className={classes}>
|
||||
<RoomAvatar room={space} width={24} height={24} />
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
});
|
||||
|
||||
spaceOptionSection = (
|
||||
<Dropdown
|
||||
id="mx_SpaceSelectDropdown"
|
||||
onOptionChange={(key: string) => {
|
||||
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
|
||||
}}
|
||||
value={selectedSpace.roomId}
|
||||
label={_t("Space selection")}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>
|
||||
);
|
||||
} else {
|
||||
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
}
|
||||
|
||||
const title = <React.Fragment>
|
||||
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
||||
<div>
|
||||
<h1>{ _t("Add existing rooms") }</h1>
|
||||
{ spaceOptionSection }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
return <BaseDialog
|
||||
title={title}
|
||||
className="mx_AddExistingToSpaceDialog"
|
||||
|
@ -293,21 +317,16 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
<MatrixClientContext.Provider value={cli}>
|
||||
<AddExistingToSpace
|
||||
space={space}
|
||||
selected={selectedToAdd}
|
||||
onChange={!busy && !error ? (checked, room) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(room);
|
||||
} else {
|
||||
selectedToAdd.delete(room);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
} : null}
|
||||
onFinished={onFinished}
|
||||
footerPrompt={<>
|
||||
<div>{ _t("Want to add a new room instead?") }</div>
|
||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</>}
|
||||
/>
|
||||
</MatrixClientContext.Provider>
|
||||
|
||||
<div className="mx_AddExistingToSpaceDialog_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
|
73
src/components/views/dialogs/UntrustedDeviceDialog.tsx
Normal file
73
src/components/views/dialogs/UntrustedDeviceDialog.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2019, 2020, 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 React from "react";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import { IDevice } from "../right_panel/UserInfo";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
user: User;
|
||||
device: IDevice;
|
||||
}
|
||||
|
||||
const UntrustedDeviceDialog: React.FC<IProps> = ({device, user, onFinished}) => {
|
||||
let askToVerifyText;
|
||||
let newSessionText;
|
||||
|
||||
if (MatrixClientPeg.get().getUserId() === user.userId) {
|
||||
newSessionText = _t("You signed in to a new session without verifying it:");
|
||||
askToVerifyText = _t("Verify your other session using one of the options below.");
|
||||
} else {
|
||||
newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:",
|
||||
{name: user.displayName, userId: user.userId});
|
||||
askToVerifyText = _t("Ask this user to verify their session, or manually verify it below.");
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
onFinished={onFinished}
|
||||
className="mx_UntrustedDeviceDialog"
|
||||
title={<>
|
||||
<E2EIcon status="warning" size={24} hideTooltip={true} />
|
||||
{ _t("Not Trusted")}
|
||||
</>}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>{newSessionText}</p>
|
||||
<p>{device.getDisplayName()} ({device.deviceId})</p>
|
||||
<p>{askToVerifyText}</p>
|
||||
</div>
|
||||
<div className='mx_Dialog_buttons'>
|
||||
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("legacy")}>
|
||||
{ _t("Manually Verify by Text") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("sas")}>
|
||||
{ _t("Interactively verify by Emoji") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={() => onFinished(false)}>
|
||||
{ _t("Done") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default UntrustedDeviceDialog;
|
|
@ -207,6 +207,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
a.href = this.props.src;
|
||||
a.download = this.props.name;
|
||||
a.target = "_blank";
|
||||
a.rel = "noreferrer noopener";
|
||||
a.click();
|
||||
};
|
||||
|
||||
|
@ -442,16 +443,16 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
<div className="mx_ImageView_panel">
|
||||
{info}
|
||||
<div className="mx_ImageView_toolbar">
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||
title={_t("Rotate Right")}
|
||||
onClick={this.onRotateClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||
title={_t("Rotate Left")}
|
||||
onClick={ this.onRotateCounterClockwiseClick }>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||
title={_t("Rotate Right")}
|
||||
onClick={this.onRotateClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
{zoomOutButton}
|
||||
{zoomInButton}
|
||||
<AccessibleTooltipButton
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 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.
|
||||
|
@ -14,29 +14,68 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import { aboveLeftOf, ContextMenu, useContextMenu } from "../../structures/ContextMenu";
|
||||
import ReactionPicker from "../emojipicker/ReactionPicker";
|
||||
import ReactionsRowButton from "./ReactionsRowButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
// The maximum number of reactions to initially show on a message.
|
||||
const MAX_ITEMS_WHEN_LIMITED = 8;
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRow")
|
||||
export default class ReactionsRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: PropTypes.object,
|
||||
const ReactButton = ({ mxEvent, reactions }: IProps) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
|
||||
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
return <React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className={classNames("mx_ReactionsRow_addReactionButton", {
|
||||
mx_ReactionsRow_addReactionButton_active: menuDisplayed,
|
||||
})}
|
||||
title={_t("Add reaction")}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: MatrixEvent;
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions?: Relations;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
myReactions: MatrixEvent[];
|
||||
showAll: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRow")
|
||||
export default class ReactionsRow extends React.PureComponent<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
|
@ -92,7 +131,7 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
if (!reactions) {
|
||||
return null;
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const userId = this.context.getUserId();
|
||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||
if (!myReactions) {
|
||||
return null;
|
||||
|
@ -114,7 +153,6 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
||||
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
const count = events.size;
|
||||
if (!count) {
|
||||
|
@ -151,13 +189,21 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
</a>;
|
||||
}
|
||||
|
||||
const cli = this.context;
|
||||
|
||||
let addReactionButton;
|
||||
if (cli.getRoom(mxEvent.getRoomId()).currentState.maySendEvent(EventType.Reaction, cli.getUserId())) {
|
||||
addReactionButton = <ReactButton mxEvent={mxEvent} reactions={reactions} />;
|
||||
}
|
||||
|
||||
return <div
|
||||
className="mx_ReactionsRow"
|
||||
role="toolbar"
|
||||
aria-label={_t("Reactions")}
|
||||
>
|
||||
{items}
|
||||
{showAllButton}
|
||||
{ items }
|
||||
{ showAllButton }
|
||||
{ addReactionButton }
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 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.
|
||||
|
@ -14,49 +14,54 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: MatrixEvent;
|
||||
// The reaction content / key / emoji
|
||||
content: string;
|
||||
// The count of votes for this key
|
||||
count: number;
|
||||
// A Set of Matrix reaction events for this key
|
||||
reactionEvents: Set<MatrixEvent>;
|
||||
// A possible Matrix event if the current user has voted for this type
|
||||
myReactionEvent?: MatrixEvent;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
tooltipRendered: boolean;
|
||||
tooltipVisible: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRowButton")
|
||||
export default class ReactionsRowButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
// The count of votes for this key
|
||||
count: PropTypes.number.isRequired,
|
||||
// A Set of Martix reaction events for this key
|
||||
reactionEvents: PropTypes.object.isRequired,
|
||||
// A possible Matrix event if the current user has voted for this type
|
||||
myReactionEvent: PropTypes.object,
|
||||
}
|
||||
export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
state = {
|
||||
tooltipRendered: false,
|
||||
tooltipVisible: false,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
tooltipVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
onClick = () => {
|
||||
const { mxEvent, myReactionEvent, content } = this.props;
|
||||
if (myReactionEvent) {
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.context.redactEvent(
|
||||
mxEvent.getRoomId(),
|
||||
myReactionEvent.getId(),
|
||||
);
|
||||
} else {
|
||||
MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", {
|
||||
this.context.sendEvent(mxEvent.getRoomId(), "m.reaction", {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": mxEvent.getId(),
|
||||
|
@ -83,8 +88,6 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const ReactionsRowButtonTooltip =
|
||||
sdk.getComponent('messages.ReactionsRowButtonTooltip');
|
||||
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -102,7 +105,7 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
/>;
|
||||
}
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
||||
const room = this.context.getRoom(mxEvent.getRoomId());
|
||||
let label;
|
||||
if (room) {
|
||||
const senders = [];
|
||||
|
@ -130,7 +133,6 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
const isPeeking = room.getMyMembership() !== "join";
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <AccessibleButton
|
||||
className={classes}
|
||||
aria-label={label}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -14,33 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { unicodeToShortcode } from '../../../HtmlUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: MatrixEvent;
|
||||
// The reaction content / key / emoji
|
||||
content: string;
|
||||
// A Set of Matrix reaction events for this key
|
||||
reactionEvents: Set<MatrixEvent>;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRowButtonTooltip")
|
||||
export default class ReactionsRowButtonTooltip extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
// A Set of Martix reaction events for this key
|
||||
reactionEvents: PropTypes.object.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
}
|
||||
export default class ReactionsRowButtonTooltip extends React.PureComponent<IProps> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
render() {
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const { content, reactionEvents, mxEvent, visible } = this.props;
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
||||
const room = this.context.getRoom(mxEvent.getRoomId());
|
||||
let tooltipLabel;
|
||||
if (room) {
|
||||
const senders = [];
|
|
@ -67,7 +67,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
|||
import RoomName from "../elements/RoomName";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
interface IDevice {
|
||||
export interface IDevice {
|
||||
deviceId: string;
|
||||
ambiguous?: boolean;
|
||||
getDisplayName(): string;
|
||||
|
|
|
@ -35,6 +35,7 @@ import {Action} from "../../../dispatcher/actions";
|
|||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
|
@ -122,6 +123,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
saveDisabled: true,
|
||||
};
|
||||
this._createEditorModel();
|
||||
window.addEventListener("beforeunload", this._saveStoredEditorState);
|
||||
}
|
||||
|
||||
_setEditorRef = ref => {
|
||||
|
@ -175,11 +177,55 @@ export default class EditMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
get _editorRoomKey() {
|
||||
return `mx_edit_room_${this._getRoom().roomId}`;
|
||||
}
|
||||
|
||||
get _editorStateKey() {
|
||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
||||
}
|
||||
|
||||
_cancelEdit = () => {
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "edit_event", event: null});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
|
||||
get _shouldSaveStoredEditorState() {
|
||||
return localStorage.getItem(this._editorRoomKey) !== null;
|
||||
}
|
||||
|
||||
_restoreStoredEditorState(partCreator) {
|
||||
const json = localStorage.getItem(this._editorStateKey);
|
||||
if (json) {
|
||||
try {
|
||||
const {parts: serializedParts} = JSON.parse(json);
|
||||
const parts = serializedParts.map(p => partCreator.deserializePart(p));
|
||||
return parts;
|
||||
} catch (e) {
|
||||
console.error("Error parsing editing state: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clearStoredEditorState() {
|
||||
localStorage.removeItem(this._editorRoomKey);
|
||||
localStorage.removeItem(this._editorStateKey);
|
||||
}
|
||||
|
||||
_clearPreviousEdit() {
|
||||
if (localStorage.getItem(this._editorRoomKey)) {
|
||||
localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`);
|
||||
}
|
||||
}
|
||||
|
||||
_saveStoredEditorState() {
|
||||
const item = SendHistoryManager.createItem(this.model);
|
||||
this._clearPreviousEdit();
|
||||
localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId());
|
||||
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
|
||||
}
|
||||
|
||||
_isSlashCommand() {
|
||||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
|
@ -266,6 +312,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
const editedEvent = this.props.editState.getEvent();
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
||||
let shouldSend = true;
|
||||
|
||||
// If content is modified then send an updated event into the room
|
||||
|
@ -311,6 +358,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
if (shouldSend) {
|
||||
this._cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
}
|
||||
|
@ -346,6 +394,10 @@ export default class EditMessageComposer extends React.Component {
|
|||
// then when mounting the editor again with the same editor state,
|
||||
// it will set the cursor at the end.
|
||||
this.props.editState.setEditorState(caret, parts);
|
||||
window.removeEventListener("beforeunload", this._saveStoredEditorState);
|
||||
if (this._shouldSaveStoredEditorState) {
|
||||
this._saveStoredEditorState();
|
||||
}
|
||||
}
|
||||
|
||||
_createEditorModel() {
|
||||
|
@ -358,10 +410,11 @@ export default class EditMessageComposer extends React.Component {
|
|||
// restore serialized parts from the state
|
||||
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||
} else {
|
||||
// otherwise, parse the body of the event
|
||||
parts = parseEvent(editState.getEvent(), partCreator);
|
||||
//otherwise, either restore serialized parts from localStorage or parse the body of the event
|
||||
parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator);
|
||||
}
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
this._saveStoredEditorState();
|
||||
}
|
||||
|
||||
_getInitialCaretPosition() {
|
||||
|
|
|
@ -23,11 +23,9 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|||
import Analytics from "../../../Analytics";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -84,8 +82,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
|
||||
public render(): React.ReactElement {
|
||||
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
|
||||
const roomTags = RoomListStore.instance.getTagsForRoom(r);
|
||||
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
|
||||
return (
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_RoomBreadcrumbs_crumb"
|
||||
|
@ -98,7 +94,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
<DecoratedRoomAvatar
|
||||
room={r}
|
||||
avatarSize={32}
|
||||
tag={roomTag}
|
||||
displayBadge={true}
|
||||
forceCount={true}
|
||||
/>
|
||||
|
|
|
@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
@ -177,7 +176,6 @@ export default class RoomHeader extends React.Component {
|
|||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={32}
|
||||
tag={DefaultTagID.Untagged} // to apply room publicity badging
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
|
|
|
@ -576,7 +576,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
const roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={32}
|
||||
tag={this.props.tag}
|
||||
displayBadge={this.props.isMinimized}
|
||||
oobData={({avatarUrl: roomProfile.avatarMxc})}
|
||||
/>;
|
||||
|
|
|
@ -232,7 +232,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
<p>
|
||||
{this.state.enabling
|
||||
? <InlineSpinner />
|
||||
: _t("Message search initilisation failed")
|
||||
: _t("Message search initialisation failed")
|
||||
}
|
||||
</p>
|
||||
{EventIndexPeg.error && (
|
||||
|
|
|
@ -23,6 +23,7 @@ import {copyPlaintext} from "../../../utils/strings";
|
|||
import {sleep} from "../../../utils/promise";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import {showRoomInviteDialog} from "../../../RoomInvite";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -50,7 +51,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
|
|||
<h3>{ _t("Share invite link") }</h3>
|
||||
<span>{ copiedText }</span>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
{ space.canInvite(MatrixClientPeg.get()?.getUserId()) ? <AccessibleButton
|
||||
className="mx_SpacePublicShare_inviteButton"
|
||||
onClick={() => {
|
||||
showRoomInviteDialog(space.roomId);
|
||||
|
@ -59,7 +60,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
|
|||
>
|
||||
<h3>{ _t("Invite people") }</h3>
|
||||
<span>{ _t("Invite with email or username") }</span>
|
||||
</AccessibleButton>
|
||||
</AccessibleButton> : null }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -209,7 +209,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
const userId = this.context.getUserId();
|
||||
|
||||
let inviteOption;
|
||||
if (this.props.space.canInvite(userId)) {
|
||||
if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) {
|
||||
inviteOption = (
|
||||
<IconizedContextMenuOption
|
||||
className="mx_SpacePanel_contextMenu_inviteButton"
|
||||
|
|
|
@ -57,8 +57,8 @@ export default class PlaybackWaveform extends React.PureComponent<IProps, IState
|
|||
};
|
||||
|
||||
private onTimeUpdate = (time: number[]) => {
|
||||
// Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar.
|
||||
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1));
|
||||
// Track percentages to a general precision to avoid over-waking the component.
|
||||
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(3));
|
||||
this.setState({progress});
|
||||
};
|
||||
|
||||
|
|
|
@ -578,14 +578,6 @@
|
|||
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark",
|
||||
"You signed in to a new session without verifying it:": "You signed in to a new session without verifying it:",
|
||||
"Verify your other session using one of the options below.": "Verify your other session using one of the options below.",
|
||||
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:",
|
||||
"Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.",
|
||||
"Not Trusted": "Not Trusted",
|
||||
"Manually Verify by Text": "Manually Verify by Text",
|
||||
"Interactively verify by Emoji": "Interactively verify by Emoji",
|
||||
"Done": "Done",
|
||||
"%(displayName)s is typing …": "%(displayName)s is typing …",
|
||||
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
|
||||
"%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
|
||||
|
@ -1097,7 +1089,7 @@
|
|||
"Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
|
||||
"%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.",
|
||||
"%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.",
|
||||
"Message search initilisation failed": "Message search initilisation failed",
|
||||
"Message search initialisation failed": "Message search initialisation failed",
|
||||
"Connecting to integration manager...": "Connecting to integration manager...",
|
||||
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
||||
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
||||
|
@ -1864,6 +1856,7 @@
|
|||
"You sent a verification request": "You sent a verification request",
|
||||
"Error decrypting video": "Error decrypting video",
|
||||
"Error processing voice message": "Error processing voice message",
|
||||
"Add reaction": "Add reaction",
|
||||
"Show all": "Show all",
|
||||
"Reactions": "Reactions",
|
||||
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
|
||||
|
@ -1948,8 +1941,8 @@
|
|||
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
|
||||
"Zoom out": "Zoom out",
|
||||
"Zoom in": "Zoom in",
|
||||
"Rotate Right": "Rotate Right",
|
||||
"Rotate Left": "Rotate Left",
|
||||
"Rotate Right": "Rotate Right",
|
||||
"Download": "Download",
|
||||
"Information": "Information",
|
||||
"View message": "View message",
|
||||
|
@ -2040,15 +2033,15 @@
|
|||
"Add a new server...": "Add a new server...",
|
||||
"%(networkName)s rooms": "%(networkName)s rooms",
|
||||
"Matrix rooms": "Matrix rooms",
|
||||
"Not all selected were added": "Not all selected were added",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
|
||||
"Filter your rooms and spaces": "Filter your rooms and spaces",
|
||||
"Feeling experimental?": "Feeling experimental?",
|
||||
"You can add existing spaces to a space.": "You can add existing spaces to a space.",
|
||||
"Direct Messages": "Direct Messages",
|
||||
"Space selection": "Space selection",
|
||||
"Add existing rooms": "Add existing rooms",
|
||||
"Not all selected were added": "Not all selected were added",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
|
||||
"Want to add a new room instead?": "Want to add a new room instead?",
|
||||
"Create a new room": "Create a new room",
|
||||
"Matrix ID": "Matrix ID",
|
||||
|
@ -2066,6 +2059,7 @@
|
|||
"Close dialog": "Close dialog",
|
||||
"Beta feedback": "Beta feedback",
|
||||
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
|
||||
"Done": "Done",
|
||||
"%(featureName)s beta feedback": "%(featureName)s beta feedback",
|
||||
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
|
||||
"To leave the beta, visit your settings.": "To leave the beta, visit your settings.",
|
||||
|
@ -2389,6 +2383,13 @@
|
|||
"Summary": "Summary",
|
||||
"Document": "Document",
|
||||
"Next": "Next",
|
||||
"You signed in to a new session without verifying it:": "You signed in to a new session without verifying it:",
|
||||
"Verify your other session using one of the options below.": "Verify your other session using one of the options below.",
|
||||
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:",
|
||||
"Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.",
|
||||
"Not Trusted": "Not Trusted",
|
||||
"Manually Verify by Text": "Manually Verify by Text",
|
||||
"Interactively verify by Emoji": "Interactively verify by Emoji",
|
||||
"Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)",
|
||||
"Upload files": "Upload files",
|
||||
"Upload all": "Upload all",
|
||||
|
@ -2703,13 +2704,12 @@
|
|||
"Failed to create initial space rooms": "Failed to create initial space rooms",
|
||||
"Skip for now": "Skip for now",
|
||||
"Creating rooms...": "Creating rooms...",
|
||||
"Failed to add rooms to space": "Failed to add rooms to space",
|
||||
"Adding...": "Adding...",
|
||||
"What do you want to organise?": "What do you want to organise?",
|
||||
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
|
||||
"Share %(name)s": "Share %(name)s",
|
||||
"It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
|
||||
"Go to my first room": "Go to my first room",
|
||||
"Go to my space": "Go to my space",
|
||||
"Who are you working with?": "Who are you working with?",
|
||||
"Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s",
|
||||
"Just me": "Just me",
|
||||
|
|
|
@ -254,11 +254,15 @@ matrixLinkify.options = {
|
|||
|
||||
target: function(href, type) {
|
||||
if (type === 'url') {
|
||||
const transformed = tryTransformPermalinkToLocalHref(href);
|
||||
if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) {
|
||||
return null;
|
||||
} else {
|
||||
return '_blank';
|
||||
try {
|
||||
const transformed = tryTransformPermalinkToLocalHref(href);
|
||||
if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) {
|
||||
return null;
|
||||
} else {
|
||||
return '_blank';
|
||||
}
|
||||
} catch (e) {
|
||||
// malformed URI
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -16,14 +16,12 @@ limitations under the License.
|
|||
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { arrayHasDiff } from "../utils/arrays";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
|
||||
const MAX_ROOMS = 20; // arbitrary
|
||||
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
|
||||
|
@ -61,30 +59,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
|||
return this.matrixClient && this.matrixClient.getVisibleRooms().length >= 20;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const prevRoomCount = (prevState.rooms?.length || 0);
|
||||
const currentRoomCount = (this.state.rooms?.length || 0)
|
||||
|
||||
// Only decrypting the breadcrumb rooms events on app initialisation
|
||||
// when room count transitions from 0 to the number of rooms it contains
|
||||
if (prevRoomCount === 0 && currentRoomCount > prevRoomCount) {
|
||||
const client = MatrixClientPeg.get();
|
||||
// Rooms in the breadcrumb have a good chance to be interacted with
|
||||
// again by a user. Decrypting the messages ahead of time will help
|
||||
// reduce content shift on first render
|
||||
this.state.rooms?.forEach(async room => {
|
||||
const [cryptoEvent] = room.currentState.getStateEvents(EventType.RoomEncryption);
|
||||
if (cryptoEvent) {
|
||||
if (!client.isRoomEncrypted(room.roomId)) {
|
||||
// XXX: Private member access
|
||||
await client._crypto.onCryptoEvent(cryptoEvent);
|
||||
}
|
||||
room?.decryptAllEvents();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
if (!this.matrixClient) return;
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
|||
import { getListAlgorithmInstance } from "./list-ordering";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
||||
import { MultiLock } from "../../../utils/MultiLock";
|
||||
|
||||
/**
|
||||
* Fired when the Algorithm has determined a list has been updated.
|
||||
|
@ -77,6 +78,7 @@ export class Algorithm extends EventEmitter {
|
|||
} = {};
|
||||
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
||||
private allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
||||
private handlerLock = new MultiLock();
|
||||
|
||||
/**
|
||||
* Set to true to suspend emissions of algorithm updates.
|
||||
|
@ -679,191 +681,204 @@ export class Algorithm extends EventEmitter {
|
|||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
|
||||
console.log(`Acquiring lock for ${room.roomId} with cause ${cause}`);
|
||||
}
|
||||
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
||||
|
||||
// Note: check the isSticky against the room ID just in case the reference is wrong
|
||||
const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId;
|
||||
if (cause === RoomUpdateCause.NewRoom) {
|
||||
const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
|
||||
const roomTags = this.roomIdsToTags[room.roomId];
|
||||
const hasTags = roomTags && roomTags.length > 0;
|
||||
|
||||
// Don't change the cause if the last sticky room is being re-added. If we fail to
|
||||
// pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus
|
||||
// lose the room.
|
||||
if (hasTags && !isForLastSticky) {
|
||||
console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
|
||||
cause = RoomUpdateCause.PossibleTagChange;
|
||||
const release = await this.handlerLock.acquire(room.roomId);
|
||||
try {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
|
||||
}
|
||||
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
||||
|
||||
// Check to see if the room is known first
|
||||
let knownRoomRef = this.rooms.includes(room);
|
||||
if (hasTags && !knownRoomRef) {
|
||||
console.warn(`${room.roomId} might be a reference change - attempting to update reference`);
|
||||
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r);
|
||||
knownRoomRef = this.rooms.includes(room);
|
||||
if (!knownRoomRef) {
|
||||
console.warn(`${room.roomId} is still not referenced. It may be sticky.`);
|
||||
// Note: check the isSticky against the room ID just in case the reference is wrong
|
||||
const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId;
|
||||
if (cause === RoomUpdateCause.NewRoom) {
|
||||
const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
|
||||
const roomTags = this.roomIdsToTags[room.roomId];
|
||||
const hasTags = roomTags && roomTags.length > 0;
|
||||
|
||||
// Don't change the cause if the last sticky room is being re-added. If we fail to
|
||||
// pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus
|
||||
// lose the room.
|
||||
if (hasTags && !isForLastSticky) {
|
||||
console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
|
||||
cause = RoomUpdateCause.PossibleTagChange;
|
||||
}
|
||||
|
||||
// Check to see if the room is known first
|
||||
let knownRoomRef = this.rooms.includes(room);
|
||||
if (hasTags && !knownRoomRef) {
|
||||
console.warn(`${room.roomId} might be a reference change - attempting to update reference`);
|
||||
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r);
|
||||
knownRoomRef = this.rooms.includes(room);
|
||||
if (!knownRoomRef) {
|
||||
console.warn(`${room.roomId} is still not referenced. It may be sticky.`);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have tags for a room and don't have the room referenced, something went horribly
|
||||
// wrong - the reference should have been updated above.
|
||||
if (hasTags && !knownRoomRef && !isSticky) {
|
||||
throw new Error(`${room.roomId} is missing from room array but is known`);
|
||||
}
|
||||
|
||||
// Like above, update the reference to the sticky room if we need to
|
||||
if (hasTags && isSticky) {
|
||||
// Go directly in and set the sticky room's new reference, being careful not
|
||||
// to trigger a sticky room update ourselves.
|
||||
this._stickyRoom.room = room;
|
||||
}
|
||||
|
||||
// If after all that we're still a NewRoom update, add the room if applicable.
|
||||
// We don't do this for the sticky room (because it causes duplication issues)
|
||||
// or if we know about the reference (as it should be replaced).
|
||||
if (cause === RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) {
|
||||
this.rooms.push(room);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have tags for a room and don't have the room referenced, something went horribly
|
||||
// wrong - the reference should have been updated above.
|
||||
if (hasTags && !knownRoomRef && !isSticky) {
|
||||
throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`);
|
||||
}
|
||||
let didTagChange = false;
|
||||
if (cause === RoomUpdateCause.PossibleTagChange) {
|
||||
const oldTags = this.roomIdsToTags[room.roomId] || [];
|
||||
const newTags = this.getTagsForRoom(room);
|
||||
const diff = arrayDiff(oldTags, newTags);
|
||||
if (diff.removed.length > 0 || diff.added.length > 0) {
|
||||
for (const rmTag of diff.removed) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Removing ${room.roomId} from ${rmTag}`);
|
||||
}
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
|
||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
|
||||
}
|
||||
for (const addTag of diff.added) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Adding ${room.roomId} to ${addTag}`);
|
||||
}
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
|
||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
|
||||
this._cachedRooms[addTag] = algorithm.orderedRooms;
|
||||
}
|
||||
|
||||
// Like above, update the reference to the sticky room if we need to
|
||||
if (hasTags && isSticky) {
|
||||
// Go directly in and set the sticky room's new reference, being careful not
|
||||
// to trigger a sticky room update ourselves.
|
||||
this._stickyRoom.room = room;
|
||||
}
|
||||
// Update the tag map so we don't regen it in a moment
|
||||
this.roomIdsToTags[room.roomId] = newTags;
|
||||
|
||||
// If after all that we're still a NewRoom update, add the room if applicable.
|
||||
// We don't do this for the sticky room (because it causes duplication issues)
|
||||
// or if we know about the reference (as it should be replaced).
|
||||
if (cause === RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) {
|
||||
this.rooms.push(room);
|
||||
}
|
||||
}
|
||||
|
||||
let didTagChange = false;
|
||||
if (cause === RoomUpdateCause.PossibleTagChange) {
|
||||
const oldTags = this.roomIdsToTags[room.roomId] || [];
|
||||
const newTags = this.getTagsForRoom(room);
|
||||
const diff = arrayDiff(oldTags, newTags);
|
||||
if (diff.removed.length > 0 || diff.added.length > 0) {
|
||||
for (const rmTag of diff.removed) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Removing ${room.roomId} from ${rmTag}`);
|
||||
console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
|
||||
}
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
|
||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
|
||||
}
|
||||
for (const addTag of diff.added) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Adding ${room.roomId} to ${addTag}`);
|
||||
}
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
|
||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
|
||||
this._cachedRooms[addTag] = algorithm.orderedRooms;
|
||||
}
|
||||
|
||||
// Update the tag map so we don't regen it in a moment
|
||||
this.roomIdsToTags[room.roomId] = newTags;
|
||||
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
|
||||
}
|
||||
cause = RoomUpdateCause.Timeline;
|
||||
didTagChange = true;
|
||||
} else {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`);
|
||||
}
|
||||
cause = RoomUpdateCause.Timeline;
|
||||
}
|
||||
|
||||
if (didTagChange && isSticky) {
|
||||
// Manually update the tag for the sticky room without triggering a sticky room
|
||||
// update. The update will be handled implicitly by the sticky room handling and
|
||||
// requires no changes on our part, if we're in the middle of a sticky room change.
|
||||
if (this._lastStickyRoom) {
|
||||
this._stickyRoom = {
|
||||
room,
|
||||
tag: this.roomIdsToTags[room.roomId][0],
|
||||
position: 0, // right at the top as it changed tags
|
||||
};
|
||||
cause = RoomUpdateCause.Timeline;
|
||||
didTagChange = true;
|
||||
} else {
|
||||
// We have to clear the lock as the sticky room change will trigger updates.
|
||||
await this.setStickyRoom(room);
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`);
|
||||
}
|
||||
cause = RoomUpdateCause.Timeline;
|
||||
}
|
||||
|
||||
if (didTagChange && isSticky) {
|
||||
// Manually update the tag for the sticky room without triggering a sticky room
|
||||
// update. The update will be handled implicitly by the sticky room handling and
|
||||
// requires no changes on our part, if we're in the middle of a sticky room change.
|
||||
if (this._lastStickyRoom) {
|
||||
this._stickyRoom = {
|
||||
room,
|
||||
tag: this.roomIdsToTags[room.roomId][0],
|
||||
position: 0, // right at the top as it changed tags
|
||||
};
|
||||
} else {
|
||||
// We have to clear the lock as the sticky room change will trigger updates.
|
||||
await this.setStickyRoom(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the update is for a room change which might be the sticky room, prevent it. We
|
||||
// need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though
|
||||
// as the sticky room relies on this.
|
||||
if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
|
||||
if (this.stickyRoom === room) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
|
||||
// If the update is for a room change which might be the sticky room, prevent it. We
|
||||
// need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though
|
||||
// as the sticky room relies on this.
|
||||
if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
|
||||
if (this.stickyRoom === room) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.warn(
|
||||
`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.roomIdsToTags[room.roomId]) {
|
||||
if (CAUSES_REQUIRING_ROOM.includes(cause)) {
|
||||
if (!this.roomIdsToTags[room.roomId]) {
|
||||
if (CAUSES_REQUIRING_ROOM.includes(cause)) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`);
|
||||
console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
|
||||
}
|
||||
|
||||
// Get the tags for the room and populate the cache
|
||||
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
|
||||
|
||||
// "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(),
|
||||
// which means we should *always* have a tag to go off of.
|
||||
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
|
||||
|
||||
this.roomIdsToTags[room.roomId] = roomTags;
|
||||
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
|
||||
console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
|
||||
}
|
||||
|
||||
// Get the tags for the room and populate the cache
|
||||
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
|
||||
const tags = this.roomIdsToTags[room.roomId];
|
||||
if (!tags) {
|
||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(),
|
||||
// which means we should *always* have a tag to go off of.
|
||||
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
|
||||
let changed = didTagChange;
|
||||
for (const tag of tags) {
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||
|
||||
this.roomIdsToTags[room.roomId] = roomTags;
|
||||
await algorithm.handleRoomUpdate(room, cause);
|
||||
this._cachedRooms[tag] = algorithm.orderedRooms;
|
||||
|
||||
// Flag that we've done something
|
||||
this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
|
||||
console.log(
|
||||
`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`,
|
||||
);
|
||||
}
|
||||
return changed;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
|
||||
}
|
||||
|
||||
const tags = this.roomIdsToTags[room.roomId];
|
||||
if (!tags) {
|
||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
let changed = didTagChange;
|
||||
for (const tag of tags) {
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||
|
||||
await algorithm.handleRoomUpdate(room, cause);
|
||||
this._cachedRooms[tag] = algorithm.orderedRooms;
|
||||
|
||||
// Flag that we've done something
|
||||
this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
|
30
src/utils/MultiLock.ts
Normal file
30
src/utils/MultiLock.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
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 { EnhancedMap } from "./maps";
|
||||
import AwaitLock from "await-lock";
|
||||
|
||||
export type DoneFn = () => void;
|
||||
|
||||
export class MultiLock {
|
||||
private locks = new EnhancedMap<string, AwaitLock>();
|
||||
|
||||
public async acquire(key: string): Promise<DoneFn> {
|
||||
const lock = this.locks.getOrCreate(key, new AwaitLock());
|
||||
await lock.acquireAsync();
|
||||
return () => lock.release();
|
||||
}
|
||||
}
|
|
@ -75,7 +75,8 @@ export function arraySmoothingResample(input: number[], points: number): number[
|
|||
for (let i = 1; i < input.length - 1; i += 2) {
|
||||
const prevPoint = input[i - 1];
|
||||
const nextPoint = input[i + 1];
|
||||
const average = (prevPoint + nextPoint) / 2;
|
||||
const currPoint = input[i];
|
||||
const average = (prevPoint + nextPoint + currPoint) / 3;
|
||||
samples.push(average);
|
||||
}
|
||||
input = samples;
|
||||
|
|
|
@ -346,9 +346,14 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string {
|
|||
return permalink;
|
||||
}
|
||||
|
||||
const m = decodeURIComponent(permalink).match(matrixLinkify.ELEMENT_URL_PATTERN);
|
||||
if (m) {
|
||||
return m[1];
|
||||
try {
|
||||
const m = decodeURIComponent(permalink).match(matrixLinkify.ELEMENT_URL_PATTERN);
|
||||
if (m) {
|
||||
return m[1];
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a valid URI
|
||||
return permalink;
|
||||
}
|
||||
|
||||
// A bit of a hack to convert permalinks of unknown origin to Element links
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020, 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.
|
||||
|
@ -14,16 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import {RightPanelPhases} from "./stores/RightPanelStorePhases";
|
||||
import {findDMForUser} from './createRoom';
|
||||
import {accessSecretStorage} from './SecurityManager';
|
||||
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||
import {Action} from './dispatcher/actions';
|
||||
import { RightPanelPhases } from "./stores/RightPanelStorePhases";
|
||||
import { findDMForUser } from './createRoom';
|
||||
import { accessSecretStorage } from './SecurityManager';
|
||||
import { verificationMethods } from 'matrix-js-sdk/src/crypto';
|
||||
import { Action } from './dispatcher/actions';
|
||||
import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog";
|
||||
import {IDevice} from "./components/views/right_panel/UserInfo";
|
||||
|
||||
async function enable4SIfNeeded() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -39,40 +42,7 @@ async function enable4SIfNeeded() {
|
|||
return true;
|
||||
}
|
||||
|
||||
function UntrustedDeviceDialog(props) {
|
||||
const {device, user, onFinished} = props;
|
||||
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
let askToVerifyText;
|
||||
let newSessionText;
|
||||
|
||||
if (MatrixClientPeg.get().getUserId() === user.userId) {
|
||||
newSessionText = _t("You signed in to a new session without verifying it:");
|
||||
askToVerifyText = _t("Verify your other session using one of the options below.");
|
||||
} else {
|
||||
newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:",
|
||||
{name: user.displayName, userId: user.userId});
|
||||
askToVerifyText = _t("Ask this user to verify their session, or manually verify it below.");
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
onFinished={onFinished}
|
||||
headerImage={require("../res/img/e2e/warning.svg")}
|
||||
title={_t("Not Trusted")}>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>{newSessionText}</p>
|
||||
<p>{device.getDisplayName()} ({device.deviceId})</p>
|
||||
<p>{askToVerifyText}</p>
|
||||
</div>
|
||||
<div className='mx_Dialog_buttons'>
|
||||
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("legacy")}>{_t("Manually Verify by Text")}</AccessibleButton>
|
||||
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("sas")}>{_t("Interactively verify by Emoji")}</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={() => onFinished()}>{_t("Done")}</AccessibleButton>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
}
|
||||
|
||||
export async function verifyDevice(user, device) {
|
||||
export async function verifyDevice(user: User, device: IDevice) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
|
@ -115,7 +85,7 @@ export async function verifyDevice(user, device) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function legacyVerifyUser(user) {
|
||||
export async function legacyVerifyUser(user: User) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
|
@ -135,7 +105,7 @@ export async function legacyVerifyUser(user) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function verifyUser(user) {
|
||||
export async function verifyUser(user: User) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
|
@ -155,7 +125,7 @@ export async function verifyUser(user) {
|
|||
});
|
||||
}
|
||||
|
||||
export function pendingVerificationRequestForUser(user) {
|
||||
export function pendingVerificationRequestForUser(user: User) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const dmRoom = findDMForUser(cli, user.userId);
|
||||
if (dmRoom) {
|
|
@ -15,12 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import {UPDATE_EVENT} from "../stores/AsyncStore";
|
||||
import {arrayFastResample, arrayRescale, arraySeed, arraySmoothingResample} from "../utils/arrays";
|
||||
import {SimpleObservable} from "matrix-widget-api";
|
||||
import {IDestroyable} from "../utils/IDestroyable";
|
||||
import {PlaybackClock} from "./PlaybackClock";
|
||||
import {createAudioContext, decodeOgg} from "./compat";
|
||||
import { UPDATE_EVENT } from "../stores/AsyncStore";
|
||||
import { arrayFastResample, arrayRescale, arraySeed, arraySmoothingResample } from "../utils/arrays";
|
||||
import { SimpleObservable } from "matrix-widget-api";
|
||||
import { IDestroyable } from "../utils/IDestroyable";
|
||||
import { PlaybackClock } from "./PlaybackClock";
|
||||
import { createAudioContext, decodeOgg } from "./compat";
|
||||
import { clamp } from "../utils/numbers";
|
||||
|
||||
export enum PlaybackState {
|
||||
Decoding = "decoding",
|
||||
|
@ -33,9 +34,20 @@ export const PLAYBACK_WAVEFORM_SAMPLES = 39;
|
|||
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
|
||||
|
||||
function makePlaybackWaveform(input: number[]): number[] {
|
||||
// We use a smoothing resample to keep the rough shape of the waveform the user will be seeing. We
|
||||
// then rescale so the user can see the waveform properly (loud noises == 100%).
|
||||
return arrayRescale(arraySmoothingResample(input, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
||||
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
||||
const noiseWaveform = input.map(v => Math.abs(v));
|
||||
|
||||
// Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
||||
// We also rescale the waveform to be 0-1 for the remaining function logic.
|
||||
const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
||||
|
||||
// Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled
|
||||
// waveform. Most speech happens below the 0.5 mark.
|
||||
const filtered = resampled.map(v => clamp(v, 0.1, 0.5));
|
||||
|
||||
// Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something
|
||||
// sensible. This is what we return to keep our contract of "values between zero and one".
|
||||
return arrayRescale(filtered, 0, 1);
|
||||
}
|
||||
|
||||
export class Playback extends EventEmitter implements IDestroyable {
|
||||
|
@ -126,6 +138,7 @@ export class Playback extends EventEmitter implements IDestroyable {
|
|||
this.waveformObservable.update(this.resampledWaveform);
|
||||
|
||||
this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
|
||||
this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update
|
||||
this.clock.durationSeconds = this.audioBuf.duration;
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,15 @@ export class PlaybackClock implements IDestroyable {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark the time in the audio context where the clip starts/has been loaded.
|
||||
* This is to ensure the clock isn't skewed into thinking it is ~0.5s into
|
||||
* a clip when the duration is set.
|
||||
*/
|
||||
public flagLoadTime() {
|
||||
this.clipStart = this.context.currentTime;
|
||||
}
|
||||
|
||||
public flagStart() {
|
||||
if (this.stopped) {
|
||||
this.clipStart = this.context.currentTime;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue