mirror of
https://github.com/element-hq/element-web
synced 2024-11-26 19:26:04 +03:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18088
This commit is contained in:
commit
80fd960304
32 changed files with 566 additions and 265 deletions
24
.github/workflows/typecheck.yaml
vendored
Normal file
24
.github/workflows/typecheck.yaml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
name: Type Check
|
||||
on:
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: c-hive/gha-yarn-cache@v2
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/ci/js-sdk-to-release.js
|
||||
cd node_modules/matrix-js-sdk
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
|
|
@ -124,6 +124,7 @@ $activeBorderColor: $secondary-content;
|
|||
align-items: center;
|
||||
padding: 4px 4px 4px 0;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
&.mx_SpaceButton_active {
|
||||
&:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper {
|
||||
|
|
|
@ -733,4 +733,8 @@ $hover-select-border: 4px;
|
|||
padding-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mx_MessageComposer_sendMessage {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -186,11 +186,14 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_MessageComposer_button {
|
||||
--size: 26px;
|
||||
position: relative;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
height: var(--size);
|
||||
line-height: var(--size);
|
||||
width: auto;
|
||||
padding-left: calc(var(--size) + 5px);
|
||||
border-radius: 100%;
|
||||
|
||||
&::before {
|
||||
|
@ -207,8 +210,22 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 0;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.mx_MessageComposer_closeButtonMenu {
|
||||
&::after {
|
||||
background: rgba($accent-color, 0.1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: $accent-color;
|
||||
|
@ -237,10 +254,18 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposer_buttonMenu::before {
|
||||
mask-image: url('$(res)/img/image-view/more.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposer_closeButtonMenu::before {
|
||||
transform: rotate(90deg);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.mx_MessageComposer_sendMessage {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
margin-right: 6px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 100%;
|
||||
|
@ -349,10 +374,19 @@ limitations under the License.
|
|||
margin-right: 0;
|
||||
|
||||
.mx_MessageComposer_wrapper {
|
||||
padding: 0;
|
||||
padding: 0 0 0 25px;
|
||||
}
|
||||
|
||||
.mx_MessageComposer_button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mx_MessageComposer_e2eIcon {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageComposer_Menu .mx_CallContextMenu_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
17
scripts/ci/js-sdk-to-release.js
Executable file
17
scripts/ci/js-sdk-to-release.js
Executable file
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require('fs/promises');
|
||||
|
||||
const PKGJSON = 'node_modules/matrix-js-sdk/package.json';
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
|
||||
for (const field of ['main', 'typings']) {
|
||||
if (pkgJson["matrix_lib_"+field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_"+field];
|
||||
}
|
||||
}
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
}
|
||||
|
||||
main();
|
|
@ -1,21 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
# This changes the js-sdk into 'release mode', that is:
|
||||
# * The entry point for the library is the babel-compiled lib/index.js rather than src/index.ts
|
||||
# * There's a 'typings' entry referencing the types output by tsc
|
||||
# We do this so we can test that each PR still builds / type checks correctly when built
|
||||
# against the released js-sdk, because if you do things like `import { User } from 'matrix-js-sdk';`
|
||||
# rather than `import { User } from 'matrix-js-sdk/src/models/user';` it will work fine with the
|
||||
# js-sdk in development mode but then break at release time.
|
||||
# We can't use the last release of the js-sdk though: it might not be up to date enough.
|
||||
|
||||
cd node_modules/matrix-js-sdk
|
||||
for i in main typings
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
done
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
|
@ -39,6 +39,8 @@ import {
|
|||
import { IUpload } from "./models/IUpload";
|
||||
import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials";
|
||||
import { BlurhashEncoder } from "./BlurhashEncoder";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics";
|
||||
|
||||
const MAX_WIDTH = 800;
|
||||
const MAX_HEIGHT = 600;
|
||||
|
@ -539,6 +541,10 @@ export default class ContentMessages {
|
|||
msgtype: "", // set later
|
||||
};
|
||||
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
decorateStartSendingTime(content);
|
||||
}
|
||||
|
||||
// if we have a mime type for the file, add it to the message metadata
|
||||
if (file.type) {
|
||||
content.info.mimetype = file.type;
|
||||
|
@ -614,6 +620,11 @@ export default class ContentMessages {
|
|||
}).then(function() {
|
||||
if (upload.canceled) throw new UploadCanceledError();
|
||||
const prom = matrixClient.sendMessage(roomId, content);
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
prom.then(resp => {
|
||||
sendRoundTripMetric(matrixClient, roomId, resp.event_id);
|
||||
});
|
||||
}
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, content);
|
||||
return prom;
|
||||
}, function(err) {
|
||||
|
|
|
@ -399,7 +399,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
|
||||
})}
|
||||
onClick={this.onExplore}
|
||||
title={_t("Explore rooms")}
|
||||
title={this.state.activeSpace
|
||||
? _t("Explore %(spaceName)s", { spaceName: this.state.activeSpace.name })
|
||||
: _t("Explore rooms")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -705,9 +705,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
|
||||
let willWantDateSeparator = false;
|
||||
let lastInSection = true;
|
||||
if (nextEvent) {
|
||||
willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
|
||||
lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEvent.getSender();
|
||||
if (nextEventWithTile) {
|
||||
willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEventWithTile.getDate() || new Date());
|
||||
lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEventWithTile.getSender();
|
||||
}
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
|
|
|
@ -53,6 +53,7 @@ import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
|||
import { throttle } from 'lodash';
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
|
||||
interface IProps {
|
||||
room?: Room; // if showing panels for a given room, this is set
|
||||
|
@ -60,6 +61,7 @@ interface IProps {
|
|||
user?: User; // used if we know the user ahead of opening the panel
|
||||
resizeNotifier: ResizeNotifier;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
e2eStatus?: E2EStatus;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -269,7 +271,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
case RightPanelPhases.EncryptionPanel:
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
room={this.state.phase === RightPanelPhases.SpaceMemberInfo ? this.state.space : this.props.room}
|
||||
room={this.context.getRoom(this.state.member.roomId) ?? this.props.room}
|
||||
key={roomId || this.state.member.userId}
|
||||
onClose={this.onClose}
|
||||
phase={this.state.phase}
|
||||
|
@ -319,7 +321,8 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
onClose={this.onClose}
|
||||
mxEvent={this.state.event}
|
||||
permalinkCreator={this.props.permalinkCreator} />;
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.ThreadPanel:
|
||||
|
|
|
@ -2063,7 +2063,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
? <RightPanel
|
||||
room={this.state.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} />
|
||||
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
|
||||
e2eStatus={this.state.e2eStatus} />
|
||||
: null;
|
||||
|
||||
const timelineClasses = classNames("mx_RoomView_timeline", {
|
||||
|
|
|
@ -275,8 +275,8 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
// fractional values (both too big and too small)
|
||||
// for scrollTop happen on certain browsers/platforms
|
||||
// when scrolled all the way down. E.g. Chrome 72 on debian.
|
||||
// so check difference < 1;
|
||||
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) < 1;
|
||||
// so check difference <= 1;
|
||||
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
|
||||
};
|
||||
|
||||
// returns the vertical height in the given direction that can be removed from
|
||||
|
|
|
@ -33,6 +33,7 @@ import { ActionPayload } from '../../dispatcher/payloads';
|
|||
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -40,6 +41,7 @@ interface IProps {
|
|||
resizeNotifier: ResizeNotifier;
|
||||
mxEvent: MatrixEvent;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
e2eStatus?: E2EStatus;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -50,6 +52,7 @@ interface IState {
|
|||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -110,10 +113,13 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
private updateThread = (thread?: Thread) => {
|
||||
if (thread) {
|
||||
this.setState({ thread });
|
||||
} else {
|
||||
this.forceUpdate();
|
||||
this.setState({
|
||||
thread,
|
||||
replyToEvent: thread.replyToEvent,
|
||||
});
|
||||
}
|
||||
|
||||
this.timelinePanelRef.current?.refreshTimeline();
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
|
@ -126,6 +132,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
|
@ -144,6 +151,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
|
|
|
@ -47,11 +47,14 @@ import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import ErrorDialog from '../views/dialogs/ErrorDialog';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
const READ_RECEIPT_INTERVAL_MS = 500;
|
||||
|
||||
const READ_MARKER_DEBOUNCE_MS = 100;
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
let debuglog = function(...s: any[]) {};
|
||||
|
@ -475,6 +478,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (this.props.manageReadMarkers) {
|
||||
this.doManageReadMarkers();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Debounced function to manage read markers because we don't need to
|
||||
* do this on every tiny scroll update. It also sets state which causes
|
||||
* a component update, which can in turn reset the scroll position, so
|
||||
* it's important we allow the browser to scroll a bit before running this
|
||||
* (hence trailing edge only and debounce rather than throttle because
|
||||
* we really only need to update this once the user has finished scrolling,
|
||||
* not periodically while they scroll).
|
||||
*/
|
||||
private doManageReadMarkers = debounce(() => {
|
||||
const rmPosition = this.getReadMarkerPosition();
|
||||
// we hide the read marker when it first comes onto the screen, but if
|
||||
// it goes back off the top of the screen (presumably because the user
|
||||
|
@ -488,8 +505,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
const timeout = this.readMarkerTimeout(rmPosition);
|
||||
// NO-OP when timeout already has set to the given value
|
||||
this.readMarkerActivityTimer.changeTimeout(timeout);
|
||||
}
|
||||
};
|
||||
}, READ_MARKER_DEBOUNCE_MS, { leading: false, trailing: true });
|
||||
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
switch (payload.action) {
|
||||
|
@ -1179,6 +1195,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
this.setState(this.getEvents());
|
||||
}
|
||||
|
||||
// Force refresh the timeline before threads support pending events
|
||||
public refreshTimeline(): void {
|
||||
this.loadTimeline();
|
||||
this.reloadEvents();
|
||||
}
|
||||
|
||||
// get the list of events from the timeline window and the pending event list
|
||||
private getEvents(): Pick<IState, "events" | "liveEvents" | "firstVisibleEventIndex"> {
|
||||
const events: MatrixEvent[] = this.timelineWindow.getEvents();
|
||||
|
|
|
@ -168,7 +168,7 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
|
|||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.SpaceMemberList,
|
||||
refireParams: { space: space },
|
||||
refireParams: { space },
|
||||
});
|
||||
onFinished();
|
||||
};
|
||||
|
|
|
@ -79,7 +79,10 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
|||
ROOM_SECURITY_TAB,
|
||||
_td("Security & Privacy"),
|
||||
"mx_RoomSettingsDialog_securityIcon",
|
||||
<SecurityRoomSettingsTab roomId={this.props.roomId} />,
|
||||
<SecurityRoomSettingsTab
|
||||
roomId={this.props.roomId}
|
||||
closeSettingsFn={() => this.props.onFinished(true)}
|
||||
/>,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
ROOM_ROLES_TAB,
|
||||
|
|
|
@ -25,6 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
title: string;
|
||||
tooltip?: React.ReactNode;
|
||||
label?: React.ReactNode;
|
||||
tooltipClassName?: string;
|
||||
forceHide?: boolean;
|
||||
yOffset?: number;
|
||||
|
@ -84,7 +85,8 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
aria-label={title}
|
||||
>
|
||||
{ children }
|
||||
{ tip }
|
||||
{ this.props.label }
|
||||
{ (tooltip || title) && tip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
|
|||
// state to show a spinner immediately after clicking "start verification",
|
||||
// before we have a request
|
||||
const [isRequesting, setRequesting] = useState(false);
|
||||
const [phase, setPhase] = useState(request && request.phase);
|
||||
const [phase, setPhase] = useState(request?.phase);
|
||||
useEffect(() => {
|
||||
setRequest(verificationRequest);
|
||||
if (verificationRequest) {
|
||||
|
|
|
@ -1278,7 +1278,9 @@ const BasicUserInfo: React.FC<{
|
|||
// hide the Roles section for DMs as it doesn't make sense there
|
||||
if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
|
||||
memberDetails = <div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Role") }</h3>
|
||||
<h3>{ _t("Role in <RoomName/>", {}, {
|
||||
RoomName: () => <b>{ room.name }</b>,
|
||||
}) }</h3>
|
||||
<PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member as RoomMember}
|
||||
|
@ -1573,11 +1575,12 @@ const UserInfo: React.FC<IProps> = ({
|
|||
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
|
||||
if (room && phase === RightPanelPhases.EncryptionPanel) {
|
||||
previousPhase = RightPanelPhases.RoomMemberInfo;
|
||||
refireParams = { member: member };
|
||||
refireParams = { member };
|
||||
} else if (room?.isSpaceRoom() && SpaceStore.spacesEnabled) {
|
||||
previousPhase = previousPhase = RightPanelPhases.SpaceMemberList;
|
||||
refireParams = { space: room };
|
||||
} else if (room) {
|
||||
previousPhase = previousPhase = SpaceStore.spacesEnabled && room.isSpaceRoom()
|
||||
? RightPanelPhases.SpaceMemberList
|
||||
: RightPanelPhases.RoomMemberList;
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
const onEncryptionPanelClose = () => {
|
||||
|
|
|
@ -29,43 +29,27 @@ import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import {
|
||||
PHASE_READY,
|
||||
PHASE_DONE,
|
||||
PHASE_STARTED,
|
||||
PHASE_CANCELLED,
|
||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import VerificationShowSas from "../verification/VerificationShowSas";
|
||||
|
||||
// XXX: Should be defined in matrix-js-sdk
|
||||
enum VerificationPhase {
|
||||
PHASE_UNSENT,
|
||||
PHASE_REQUESTED,
|
||||
PHASE_READY,
|
||||
PHASE_DONE,
|
||||
PHASE_STARTED,
|
||||
PHASE_CANCELLED,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
layout: string;
|
||||
request: VerificationRequest;
|
||||
member: RoomMember | User;
|
||||
phase: VerificationPhase;
|
||||
phase: Phase;
|
||||
onClose: () => void;
|
||||
isRoomEncrypted: boolean;
|
||||
inDialog: boolean;
|
||||
key: number;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
sasEvent?: SAS;
|
||||
sasEvent?: SAS["sasEvent"];
|
||||
emojiButtonClicked?: boolean;
|
||||
reciprocateButtonClicked?: boolean;
|
||||
reciprocateQREvent?: ReciprocateQRCode;
|
||||
reciprocateQREvent?: ReciprocateQRCode["reciprocateQREvent"];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.right_panel.VerificationPanel")
|
||||
|
@ -321,9 +305,9 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
const displayName = (member as User).displayName || (member as RoomMember).name || member.userId;
|
||||
|
||||
switch (phase) {
|
||||
case PHASE_READY:
|
||||
case Phase.Ready:
|
||||
return this.renderQRPhase();
|
||||
case PHASE_STARTED:
|
||||
case Phase.Started:
|
||||
switch (request.chosenMethod) {
|
||||
case verificationMethods.RECIPROCATE_QR_CODE:
|
||||
return this.renderQRReciprocatePhase();
|
||||
|
@ -346,9 +330,9 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
default:
|
||||
return null;
|
||||
}
|
||||
case PHASE_DONE:
|
||||
case Phase.Done:
|
||||
return this.renderVerifiedPhase();
|
||||
case PHASE_CANCELLED:
|
||||
case Phase.Cancelled:
|
||||
return this.renderCancelledPhase();
|
||||
}
|
||||
console.error("VerificationPanel unhandled phase:", phase);
|
||||
|
@ -375,7 +359,8 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
|
||||
private updateVerifierState = () => {
|
||||
const { request } = this.props;
|
||||
const { sasEvent, reciprocateQREvent } = request.verifier;
|
||||
const sasEvent = (request.verifier as SAS).sasEvent;
|
||||
const reciprocateQREvent = (request.verifier as ReciprocateQRCode).reciprocateQREvent;
|
||||
request.verifier.off('show_sas', this.updateVerifierState);
|
||||
request.verifier.off('show_reciprocate_qr', this.updateVerifierState);
|
||||
this.setState({ sasEvent, reciprocateQREvent });
|
||||
|
@ -402,7 +387,8 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
const { request } = this.props;
|
||||
request.on("change", this.onRequestChange);
|
||||
if (request.verifier) {
|
||||
const { sasEvent, reciprocateQREvent } = request.verifier;
|
||||
const sasEvent = (request.verifier as SAS).sasEvent;
|
||||
const reciprocateQREvent = (request.verifier as ReciprocateQRCode).reciprocateQREvent;
|
||||
this.setState({ sasEvent, reciprocateQREvent });
|
||||
}
|
||||
this.onRequestChange();
|
||||
|
|
|
@ -13,7 +13,7 @@ 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 React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
|
@ -27,7 +27,12 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
|
|||
import ContentMessages from '../../../ContentMessages';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import {
|
||||
aboveLeftOf,
|
||||
ContextMenu,
|
||||
useContextMenu,
|
||||
MenuItem,
|
||||
} from "../../structures/ContextMenu";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
|
@ -45,6 +50,10 @@ import { Action } from "../../../dispatcher/actions";
|
|||
import EditorModel from "../../../editor/model";
|
||||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
||||
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
|
||||
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
||||
|
||||
let instanceCount = 0;
|
||||
const NARROW_MODE_BREAKPOINT = 500;
|
||||
|
||||
interface IComposerAvatarProps {
|
||||
me: object;
|
||||
|
@ -71,13 +80,19 @@ function SendButton(props: ISendButtonProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const EmojiButton = ({ addEmoji }) => {
|
||||
interface IEmojiButtonProps {
|
||||
addEmoji: (unicode: string) => boolean;
|
||||
menuPosition: any; // TODO: Types
|
||||
narrowMode: boolean;
|
||||
}
|
||||
|
||||
const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition, narrowMode }) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
|
||||
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
|
||||
contextMenu = <ContextMenu {...position} onFinished={closeMenu} managed={false}>
|
||||
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
@ -93,12 +108,11 @@ const EmojiButton = ({ addEmoji }) => {
|
|||
// TODO: replace ContextMenuTooltipButton with a unified representation of
|
||||
// the header buttons and the right panel buttons
|
||||
return <React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
<AccessibleTooltipButton
|
||||
className={className}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
title={_t('Emoji picker')}
|
||||
inputRef={button}
|
||||
title={!narrowMode && _t('Emoji picker')}
|
||||
label={narrowMode && _t("Add emoji")}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
|
@ -196,6 +210,9 @@ interface IState {
|
|||
haveRecording: boolean;
|
||||
recordingTimeLeftSeconds?: number;
|
||||
me?: RoomMember;
|
||||
narrowMode?: boolean;
|
||||
isMenuOpen: boolean;
|
||||
showStickers: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.MessageComposer")
|
||||
|
@ -203,6 +220,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
private dispatcherRef: string;
|
||||
private messageComposerInput: SendMessageComposer;
|
||||
private voiceRecordingButton: VoiceRecordComposerTile;
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
static defaultProps = {
|
||||
replyInThread: false,
|
||||
|
@ -220,15 +239,32 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
isComposerEmpty: true,
|
||||
haveRecording: false,
|
||||
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
|
||||
isMenuOpen: false,
|
||||
showStickers: false,
|
||||
};
|
||||
|
||||
this.instanceId = instanceCount++;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
this.waitForOwnMember();
|
||||
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current);
|
||||
UIStore.instance.on(`MessageComposer${this.instanceId}`, this.onResize);
|
||||
}
|
||||
|
||||
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
|
||||
if (type === UI_EVENTS.Resize) {
|
||||
const narrowMode = entry.contentRect.width <= NARROW_MODE_BREAKPOINT;
|
||||
this.setState({
|
||||
narrowMode,
|
||||
isMenuOpen: !narrowMode ? false : this.state.isMenuOpen,
|
||||
showStickers: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === 'reply_to_event') {
|
||||
// add a timeout for the reply preview to be rendered, so
|
||||
|
@ -263,6 +299,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
|
||||
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);
|
||||
}
|
||||
|
||||
private onRoomStateEvents = (ev, state) => {
|
||||
|
@ -312,7 +350,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
private renderPlaceholderText = () => {
|
||||
if (this.props.replyToEvent) {
|
||||
if (this.props.e2eStatus) {
|
||||
if (this.props.replyInThread && this.props.e2eStatus) {
|
||||
return _t('Reply to encrypted thread…');
|
||||
} else if (this.props.replyInThread) {
|
||||
return _t('Reply to thread…');
|
||||
} else if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} else {
|
||||
return _t('Send a reply…');
|
||||
|
@ -326,11 +368,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private addEmoji(emoji: string) {
|
||||
private addEmoji(emoji: string): boolean {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
text: emoji,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private sendMessage = async () => {
|
||||
|
@ -369,6 +412,97 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private shouldShowStickerPicker = (): boolean => {
|
||||
return SettingsStore.getValue(UIFeature.Widgets)
|
||||
&& SettingsStore.getValue("MessageComposerInput.showStickersButton")
|
||||
&& !this.state.haveRecording;
|
||||
};
|
||||
|
||||
private showStickers = (showStickers: boolean) => {
|
||||
this.setState({ showStickers });
|
||||
};
|
||||
|
||||
private toggleButtonMenu = (): void => {
|
||||
this.setState({
|
||||
isMenuOpen: !this.state.isMenuOpen,
|
||||
});
|
||||
};
|
||||
|
||||
private renderButtons(menuPosition): JSX.Element | JSX.Element[] {
|
||||
const buttons: JSX.Element[] = [];
|
||||
if (!this.state.haveRecording) {
|
||||
buttons.push(
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
buttons.push(
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} menuPosition={menuPosition} narrowMode={this.state.narrowMode} />,
|
||||
);
|
||||
}
|
||||
if (this.shouldShowStickerPicker()) {
|
||||
let title;
|
||||
if (!this.state.narrowMode) {
|
||||
title = this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers");
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<AccessibleTooltipButton
|
||||
id='stickersButton'
|
||||
key="controls_stickers"
|
||||
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
||||
onClick={() => this.showStickers(!this.state.showStickers)}
|
||||
title={title}
|
||||
label={this.state.narrowMode && _t("Send a sticker")}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
if (!this.state.haveRecording && !this.state.narrowMode) {
|
||||
buttons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
|
||||
title={_t("Send voice message")}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.narrowMode) {
|
||||
return buttons;
|
||||
} else {
|
||||
const classnames = classNames({
|
||||
mx_MessageComposer_button: true,
|
||||
mx_MessageComposer_buttonMenu: true,
|
||||
mx_MessageComposer_closeButtonMenu: this.state.isMenuOpen,
|
||||
});
|
||||
|
||||
return <>
|
||||
{ buttons[0] }
|
||||
<AccessibleTooltipButton
|
||||
className={classnames}
|
||||
onClick={this.toggleButtonMenu}
|
||||
title={_t("More options")}
|
||||
tooltip={false}
|
||||
/>
|
||||
{ this.state.isMenuOpen && (
|
||||
<ContextMenu
|
||||
onFinished={this.toggleButtonMenu}
|
||||
{...menuPosition}
|
||||
menuPaddingRight={10}
|
||||
menuPaddingTop={5}
|
||||
menuPaddingBottom={5}
|
||||
menuWidth={150}
|
||||
wrapperClassName="mx_MessageComposer_Menu"
|
||||
>
|
||||
{ buttons.slice(1).map((button, index) => (
|
||||
<MenuItem className="mx_CallContextMenu_item" key={index} onClick={this.toggleButtonMenu}>
|
||||
{ button }
|
||||
</MenuItem>
|
||||
)) }
|
||||
</ContextMenu>
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const controls = [
|
||||
this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||
|
@ -377,6 +511,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
null,
|
||||
];
|
||||
|
||||
let menuPosition;
|
||||
if (this.ref.current) {
|
||||
const contentRect = this.ref.current.getBoundingClientRect();
|
||||
menuPosition = aboveLeftOf(contentRect);
|
||||
}
|
||||
|
||||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
|
@ -392,33 +532,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
/>,
|
||||
);
|
||||
|
||||
if (!this.state.haveRecording) {
|
||||
controls.push(
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Widgets) &&
|
||||
SettingsStore.getValue("MessageComposerInput.showStickersButton") &&
|
||||
!this.state.haveRecording) {
|
||||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={c => this.voiceRecordingButton = c}
|
||||
room={this.props.room} />);
|
||||
|
||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||
controls.push(
|
||||
<SendButton
|
||||
key="controls_send"
|
||||
onClick={this.sendMessage}
|
||||
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
||||
|
@ -459,6 +576,15 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
yOffset={-50}
|
||||
/>;
|
||||
}
|
||||
controls.push(
|
||||
<Stickerpicker
|
||||
room={this.props.room}
|
||||
showStickers={this.state.showStickers}
|
||||
setShowStickers={this.showStickers}
|
||||
menuPosition={menuPosition} />,
|
||||
);
|
||||
|
||||
const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording;
|
||||
|
||||
const classes = classNames({
|
||||
"mx_MessageComposer": true,
|
||||
|
@ -467,7 +593,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={classes} ref={this.ref}>
|
||||
{ recordingTooltip }
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
{ this.props.showReplyPreview && (
|
||||
|
@ -475,6 +601,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
) }
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ controls }
|
||||
{ this.renderButtons(menuPosition) }
|
||||
{ showSendButton && (
|
||||
<SendButton
|
||||
key="controls_send"
|
||||
onClick={this.sendMessage}
|
||||
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -48,6 +48,7 @@ import SpaceStore, { ISuggestedRoom, SUGGESTED_ROOMS } from "../../../stores/Spa
|
|||
import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -522,20 +523,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
} else if (
|
||||
this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join"
|
||||
) {
|
||||
const spaceName = this.props.activeSpace.name;
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{ _t("Quick actions") }</div>
|
||||
{ this.props.activeSpace.canInvite(userId) && <AccessibleButton
|
||||
{ this.props.activeSpace.canInvite(userId) && <AccessibleTooltipButton
|
||||
className="mx_RoomList_explorePrompt_spaceInvite"
|
||||
onClick={this.onSpaceInviteClick}
|
||||
title={_t("Invite to %(spaceName)s", { spaceName })}
|
||||
>
|
||||
{ _t("Invite people") }
|
||||
</AccessibleButton> }
|
||||
{ this.props.activeSpace.getMyMembership() === "join" && <AccessibleButton
|
||||
</AccessibleTooltipButton> }
|
||||
{ this.props.activeSpace.getMyMembership() === "join" && <AccessibleTooltipButton
|
||||
className="mx_RoomList_explorePrompt_spaceExplore"
|
||||
onClick={this.onExplore}
|
||||
title={_t("Explore %(spaceName)s", { spaceName })}
|
||||
>
|
||||
{ _t("Explore rooms") }
|
||||
</AccessibleButton> }
|
||||
</AccessibleTooltipButton> }
|
||||
</div>;
|
||||
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
||||
const unfilteredLists = RoomListStore.instance.unfilteredLists;
|
||||
|
|
|
@ -54,6 +54,7 @@ import { Room } from 'matrix-js-sdk/src/models/room';
|
|||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
|
@ -418,6 +419,10 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
// don't bother sending an empty message
|
||||
if (!content.body.trim()) return;
|
||||
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
decorateStartSendingTime(content);
|
||||
}
|
||||
|
||||
const prom = this.context.sendMessage(roomId, content);
|
||||
if (replyToEvent) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
|
@ -433,6 +438,11 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
dis.dispatch({ action: `effects.${effect.command}` });
|
||||
}
|
||||
});
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
prom.then(resp => {
|
||||
sendRoundTripMetric(this.context, roomId, resp.event_id);
|
||||
});
|
||||
}
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import AppTile from '../elements/AppTile';
|
||||
|
@ -27,7 +26,6 @@ import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -44,10 +42,12 @@ const PERSISTED_ELEMENT_KEY = "stickerPicker";
|
|||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
showStickers: boolean;
|
||||
menuPosition?: any;
|
||||
setShowStickers: (showStickers: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
showStickers: boolean;
|
||||
imError: string;
|
||||
stickerpickerX: number;
|
||||
stickerpickerY: number;
|
||||
|
@ -72,7 +72,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showStickers: false,
|
||||
imError: null,
|
||||
stickerpickerX: null,
|
||||
stickerpickerY: null,
|
||||
|
@ -114,7 +113,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
console.warn('No widget ID specified, not disabling assets');
|
||||
}
|
||||
|
||||
this.setState({ showStickers: false });
|
||||
this.props.setShowStickers(false);
|
||||
WidgetUtils.removeStickerpickerWidgets().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
|
@ -146,15 +145,15 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
this.sendVisibilityToWidget(this.state.showStickers);
|
||||
this.sendVisibilityToWidget(this.props.showStickers);
|
||||
}
|
||||
|
||||
private imError(errorMsg: string, e: Error): void {
|
||||
console.error(errorMsg, e);
|
||||
this.setState({
|
||||
showStickers: false,
|
||||
imError: _t(errorMsg),
|
||||
});
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
|
||||
private updateWidget = (): void => {
|
||||
|
@ -194,12 +193,12 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
this.forceUpdate();
|
||||
break;
|
||||
case "stickerpicker_close":
|
||||
this.setState({ showStickers: false });
|
||||
this.props.setShowStickers(false);
|
||||
break;
|
||||
case Action.AfterRightPanelPhaseChange:
|
||||
case "show_left_panel":
|
||||
case "hide_left_panel":
|
||||
this.setState({ showStickers: false });
|
||||
this.props.setShowStickers(false);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -338,8 +337,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
|
||||
this.props.setShowStickers(true);
|
||||
this.setState({
|
||||
showStickers: true,
|
||||
stickerpickerX: x,
|
||||
stickerpickerY: y,
|
||||
stickerpickerChevronOffset,
|
||||
|
@ -351,8 +350,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
* @param {Event} ev Event that triggered the function call
|
||||
*/
|
||||
private onHideStickersClick = (ev: React.MouseEvent): void => {
|
||||
if (this.state.showStickers) {
|
||||
this.setState({ showStickers: false });
|
||||
if (this.props.showStickers) {
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -360,8 +359,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
* Called when the window is resized
|
||||
*/
|
||||
private onResize = (): void => {
|
||||
if (this.state.showStickers) {
|
||||
this.setState({ showStickers: false });
|
||||
if (this.props.showStickers) {
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -369,8 +368,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
* The stickers picker was hidden
|
||||
*/
|
||||
private onFinished = (): void => {
|
||||
if (this.state.showStickers) {
|
||||
this.setState({ showStickers: false });
|
||||
if (this.props.showStickers) {
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -395,26 +394,9 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
let stickerPicker;
|
||||
let stickersButton;
|
||||
const className = classNames(
|
||||
"mx_MessageComposer_button",
|
||||
"mx_MessageComposer_stickers",
|
||||
"mx_Stickers_hideStickers",
|
||||
"mx_MessageComposer_button_highlight",
|
||||
);
|
||||
if (this.state.showStickers) {
|
||||
// Show hide-stickers button
|
||||
stickersButton =
|
||||
<AccessibleButton
|
||||
id='stickersButton'
|
||||
key="controls_hide_stickers"
|
||||
className={className}
|
||||
onClick={this.onHideStickersClick}
|
||||
title={_t("Hide Stickers")}
|
||||
/>;
|
||||
if (!this.props.showStickers) return null;
|
||||
|
||||
stickerPicker = <ContextMenu
|
||||
return <ContextMenu
|
||||
chevronOffset={this.state.stickerpickerChevronOffset}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
left={this.state.stickerpickerX}
|
||||
|
@ -426,23 +408,9 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
menuPaddingLeft={0}
|
||||
menuPaddingRight={0}
|
||||
zIndex={STICKERPICKER_Z_INDEX}
|
||||
{...this.props.menuPosition}
|
||||
>
|
||||
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
|
||||
</ContextMenu>;
|
||||
} else {
|
||||
// Show show-stickers button
|
||||
stickersButton =
|
||||
<AccessibleTooltipButton
|
||||
id='stickersButton'
|
||||
key="controls_show_stickers"
|
||||
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
||||
onClick={this.onShowStickersClick}
|
||||
title={_t("Show Stickers")}
|
||||
/>;
|
||||
}
|
||||
return <React.Fragment>
|
||||
{ stickersButton }
|
||||
{ stickerPicker }
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import React, { ReactNode } from "react";
|
|||
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import LiveRecordingClock from "../audio_messages/LiveRecordingClock";
|
||||
|
@ -137,7 +136,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
await this.disposeRecording();
|
||||
};
|
||||
|
||||
private onRecordStartEndClick = async () => {
|
||||
public onRecordStartEndClick = async () => {
|
||||
if (this.state.recorder) {
|
||||
await this.state.recorder.stop();
|
||||
return;
|
||||
|
@ -215,27 +214,23 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
let stopOrRecordBtn;
|
||||
let deleteButton;
|
||||
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
||||
const classes = classNames({
|
||||
'mx_MessageComposer_button': !this.state.recorder,
|
||||
'mx_MessageComposer_voiceMessage': !this.state.recorder,
|
||||
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
||||
});
|
||||
if (!this.state.recordingPhase) return null;
|
||||
|
||||
let stopBtn;
|
||||
let deleteButton;
|
||||
if (this.state.recordingPhase === RecordingState.Started) {
|
||||
let tooltip = _t("Send voice message");
|
||||
if (!!this.state.recorder) {
|
||||
tooltip = _t("Stop recording");
|
||||
}
|
||||
|
||||
stopOrRecordBtn = <AccessibleTooltipButton
|
||||
className={classes}
|
||||
stopBtn = <AccessibleTooltipButton
|
||||
className="mx_VoiceRecordComposerTile_stop"
|
||||
onClick={this.onRecordStartEndClick}
|
||||
title={tooltip}
|
||||
/>;
|
||||
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
||||
stopOrRecordBtn = null;
|
||||
stopBtn = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,13 +259,10 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
</span>;
|
||||
}
|
||||
|
||||
// The record button (mic icon) is meant to be on the right edge, but we also want the
|
||||
// stop button to be left of the waveform area. Luckily, none of the surrounding UI is
|
||||
// rendered when we're not recording, so the record button ends up in the correct spot.
|
||||
return (<>
|
||||
{ uploadIndicator }
|
||||
{ deleteButton }
|
||||
{ stopOrRecordBtn }
|
||||
{ stopBtn }
|
||||
{ this.renderWaveformArea() }
|
||||
</>);
|
||||
}
|
||||
|
|
|
@ -39,9 +39,12 @@ import { arrayHasDiff } from "../../../../../utils/arrays";
|
|||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import createRoom, { IOpts } from '../../../../../createRoom';
|
||||
import CreateRoomDialog from '../../../dialogs/CreateRoomDialog';
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { ROOM_SECURITY_TAB } from "../../../dialogs/RoomSettingsDialog";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
closeSettingsFn: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -220,9 +223,20 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
targetVersion,
|
||||
description: _t("This upgrade will allow members of selected spaces " +
|
||||
"access to this room without an invite."),
|
||||
onFinished: (resp) => {
|
||||
onFinished: async (resp) => {
|
||||
if (!resp?.continue) return;
|
||||
upgradeRoom(room, targetVersion, resp.invite);
|
||||
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
|
||||
this.props.closeSettingsFn();
|
||||
// switch to the new room in the background
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: roomId,
|
||||
});
|
||||
// open new settings on this tab
|
||||
dis.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: ROOM_SECURITY_TAB,
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
@ -620,6 +634,22 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
historySection = null;
|
||||
}
|
||||
|
||||
let advanced;
|
||||
if (this.state.joinRule === JoinRule.Public) {
|
||||
advanced = (
|
||||
<>
|
||||
<AccessibleButton
|
||||
onClick={this.toggleAdvancedSection}
|
||||
kind="link"
|
||||
className="mx_SettingsTab_showAdvanced"
|
||||
>
|
||||
{ this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") }
|
||||
</AccessibleButton>
|
||||
{ this.state.showAdvancedSection && this.renderAdvanced() }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("Security & Privacy") }</div>
|
||||
|
@ -645,15 +675,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
{ this.renderJoinRule() }
|
||||
</div>
|
||||
|
||||
<AccessibleButton
|
||||
onClick={this.toggleAdvancedSection}
|
||||
kind="link"
|
||||
className="mx_SettingsTab_showAdvanced"
|
||||
>
|
||||
{ this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") }
|
||||
</AccessibleButton>
|
||||
{ this.state.showAdvancedSection && this.renderAdvanced() }
|
||||
|
||||
{ advanced }
|
||||
{ historySection }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -94,6 +94,7 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
|
|||
const canonicalAliasEv = space.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
|
||||
|
||||
let advancedSection;
|
||||
if (visibility === SpaceVisibility.Unlisted) {
|
||||
if (showAdvancedSection) {
|
||||
advancedSection = <>
|
||||
<AccessibleButton onClick={toggleAdvancedSection} kind="link" className="mx_SettingsTab_showAdvanced">
|
||||
|
@ -119,6 +120,7 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
|
|||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
let addressesSection;
|
||||
if (visibility !== SpaceVisibility.Private) {
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||
import { IGeneratedSas } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||
import { DeviceInfo } from "matrix-js-sdk/src//crypto/deviceinfo";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { PendingActionSpinner } from "../right_panel/EncryptionInfo";
|
||||
|
@ -30,7 +30,7 @@ interface IProps {
|
|||
device?: DeviceInfo;
|
||||
onDone: () => void;
|
||||
onCancel: () => void;
|
||||
sas: SAS.sas;
|
||||
sas: IGeneratedSas;
|
||||
isSelf?: boolean;
|
||||
inDialog?: boolean; // whether this component is being shown in a dialog and to use DialogButtons
|
||||
}
|
||||
|
|
|
@ -1554,12 +1554,19 @@
|
|||
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
||||
"Send message": "Send message",
|
||||
"Emoji picker": "Emoji picker",
|
||||
"Add emoji": "Add emoji",
|
||||
"Upload file": "Upload file",
|
||||
"Reply to encrypted thread…": "Reply to encrypted thread…",
|
||||
"Reply to thread…": "Reply to thread…",
|
||||
"Send an encrypted reply…": "Send an encrypted reply…",
|
||||
"Send a reply…": "Send a reply…",
|
||||
"Send an encrypted message…": "Send an encrypted message…",
|
||||
"Send a message…": "Send a message…",
|
||||
"Hide Stickers": "Hide Stickers",
|
||||
"Show Stickers": "Show Stickers",
|
||||
"Send a sticker": "Send a sticker",
|
||||
"Send voice message": "Send voice message",
|
||||
"More options": "More options",
|
||||
"The conversation continues here.": "The conversation continues here.",
|
||||
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
||||
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
||||
|
@ -1638,6 +1645,7 @@
|
|||
"Start a new chat": "Start a new chat",
|
||||
"Explore all public rooms": "Explore all public rooms",
|
||||
"Quick actions": "Quick actions",
|
||||
"Explore %(spaceName)s": "Explore %(spaceName)s",
|
||||
"Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below",
|
||||
"%(count)s results in all spaces|other": "%(count)s results in all spaces",
|
||||
"%(count)s results in all spaces|one": "%(count)s result in all spaces",
|
||||
|
@ -1720,8 +1728,6 @@
|
|||
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
||||
"Add some now": "Add some now",
|
||||
"Stickerpack": "Stickerpack",
|
||||
"Hide Stickers": "Hide Stickers",
|
||||
"Show Stickers": "Show Stickers",
|
||||
"Failed to revoke invite": "Failed to revoke invite",
|
||||
"Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.",
|
||||
"Admin Tools": "Admin Tools",
|
||||
|
@ -1861,7 +1867,7 @@
|
|||
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?",
|
||||
"Deactivate user": "Deactivate user",
|
||||
"Failed to deactivate user": "Failed to deactivate user",
|
||||
"Role": "Role",
|
||||
"Role in <RoomName/>": "Role in <RoomName/>",
|
||||
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
|
||||
"Edit devices": "Edit devices",
|
||||
"Security": "Security",
|
||||
|
|
46
src/sendTimePerformanceMetrics.ts
Normal file
46
src/sendTimePerformanceMetrics.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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 { MatrixClient } from "matrix-js-sdk/src";
|
||||
|
||||
/**
|
||||
* Decorates the given event content object with the "send start time". The
|
||||
* object will be modified in-place.
|
||||
* @param {object} content The event content.
|
||||
*/
|
||||
export function decorateStartSendingTime(content: object) {
|
||||
content['io.element.performance_metrics'] = {
|
||||
sendStartTs: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an event decorated with `decorateStartSendingTime()` has been sent
|
||||
* by the server (the client now knows the event ID).
|
||||
* @param {MatrixClient} client The client to send as.
|
||||
* @param {string} inRoomId The room ID where the original event was sent.
|
||||
* @param {string} forEventId The event ID for the decorated event.
|
||||
*/
|
||||
export function sendRoundTripMetric(client: MatrixClient, inRoomId: string, forEventId: string) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
client.sendEvent(inRoomId, 'io.element.performance_metric', {
|
||||
"io.element.performance_metrics": {
|
||||
forEventId: forEventId,
|
||||
responseTs: Date.now(),
|
||||
kind: 'send_time',
|
||||
},
|
||||
});
|
||||
}
|
|
@ -720,6 +720,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
default: true,
|
||||
controller: new ReducedMotionController(),
|
||||
},
|
||||
"Performance.addSendMessageTimingMetadata": {
|
||||
supportedLevels: [SettingLevel.CONFIG],
|
||||
default: false,
|
||||
},
|
||||
"Widgets.pinned": { // deprecated
|
||||
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
||||
default: {},
|
||||
|
|
|
@ -22,6 +22,7 @@ import Modal from "../Modal";
|
|||
import { _t } from "../languageHandler";
|
||||
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
||||
import SpaceStore from "../stores/SpaceStore";
|
||||
import Spinner from "../components/views/elements/Spinner";
|
||||
|
||||
export async function upgradeRoom(
|
||||
room: Room,
|
||||
|
@ -29,8 +30,10 @@ export async function upgradeRoom(
|
|||
inviteUsers = false,
|
||||
handleError = true,
|
||||
updateSpaces = true,
|
||||
awaitRoom = false,
|
||||
): Promise<string> {
|
||||
const cli = room.client;
|
||||
const spinnerModal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
|
||||
|
||||
let newRoomId: string;
|
||||
try {
|
||||
|
@ -46,14 +49,27 @@ export async function upgradeRoom(
|
|||
throw e;
|
||||
}
|
||||
|
||||
if (awaitRoom || inviteUsers) {
|
||||
await new Promise<void>(resolve => {
|
||||
// already have the room
|
||||
if (room.client.getRoom(newRoomId)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to wait for the js-sdk to give us the room back so
|
||||
// we can more effectively abuse the MultiInviter behaviour
|
||||
// which heavily relies on the Room object being available.
|
||||
if (inviteUsers) {
|
||||
const checkForUpgradeFn = async (newRoom: Room): Promise<void> => {
|
||||
// The upgradePromise should be done by the time we await it here.
|
||||
const checkForRoomFn = (newRoom: Room) => {
|
||||
if (newRoom.roomId !== newRoomId) return;
|
||||
resolve();
|
||||
cli.off("Room", checkForRoomFn);
|
||||
};
|
||||
cli.on("Room", checkForRoomFn);
|
||||
});
|
||||
}
|
||||
|
||||
if (inviteUsers) {
|
||||
const toInvite = [
|
||||
...room.getMembersWithMembership("join"),
|
||||
...room.getMembersWithMembership("invite"),
|
||||
|
@ -63,10 +79,6 @@ export async function upgradeRoom(
|
|||
// Errors are handled internally to this function
|
||||
await inviteUsersToRoom(newRoomId, toInvite);
|
||||
}
|
||||
|
||||
cli.removeListener('Room', checkForUpgradeFn);
|
||||
};
|
||||
cli.on('Room', checkForUpgradeFn);
|
||||
}
|
||||
|
||||
if (updateSpaces) {
|
||||
|
@ -89,5 +101,6 @@ export async function upgradeRoom(
|
|||
}
|
||||
}
|
||||
|
||||
spinnerModal.close();
|
||||
return newRoomId;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue