@@ -499,16 +503,24 @@ interface IPowerLevelsContent {
redact?: number;
}
-const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => {
+export const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => {
if (!powerLevelContent || !member) return false;
const levelToSend =
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
powerLevelContent.events_default;
+
+ // levelToSend could be undefined as .events_default is optional. Coercing in this case using
+ // Number() would always return false, so this preserves behaviour
+ // FIXME: per the spec, if `events_default` is unset, it defaults to zero. If
+ // the member has a negative powerlevel, this will give an incorrect result.
+ if (levelToSend === undefined) return false;
+
return member.powerLevel < levelToSend;
};
-const getPowerLevels = (room) => room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
+export const getPowerLevels = (room: Room): IPowerLevelsContent =>
+ room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
const [powerLevels, setPowerLevels] = useState
(getPowerLevels(room));
@@ -538,7 +550,7 @@ interface IBaseProps {
stopUpdating(): void;
}
-const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit) => {
+export const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit) => {
const cli = useContext(MatrixClientContext);
// check if user can be kicked/disinvited
@@ -566,7 +578,7 @@ const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit {
// Return true if the target member is not banned and we have sufficient PL to ban them
- const myMember = child.getMember(cli.credentials.userId);
+ const myMember = child.getMember(cli.credentials.userId || "");
const theirMember = child.getMember(member.userId);
return (
myMember &&
@@ -648,7 +660,7 @@ const RedactMessagesButton: React.FC = ({ member }) => {
);
};
-const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit) => {
+export const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit) => {
const cli = useContext(MatrixClientContext);
const isBanned = member.membership === "ban";
@@ -674,7 +686,7 @@ const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit {
// Return true if the target member is banned and we have sufficient PL to unban
- const myMember = child.getMember(cli.credentials.userId);
+ const myMember = child.getMember(cli.credentials.userId || "");
const theirMember = child.getMember(member.userId);
return (
myMember &&
@@ -686,7 +698,7 @@ const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit {
// Return true if the target member isn't banned and we have sufficient PL to ban
- const myMember = child.getMember(cli.credentials.userId);
+ const myMember = child.getMember(cli.credentials.userId || "");
const theirMember = child.getMember(member.userId);
return (
myMember &&
@@ -835,7 +847,7 @@ const MuteToggleButton: React.FC = ({ member, room, powerLevels,
);
};
-const RoomAdminToolsContainer: React.FC = ({
+export const RoomAdminToolsContainer: React.FC = ({
room,
children,
member,
@@ -855,7 +867,7 @@ const RoomAdminToolsContainer: React.FC = ({
// if these do not exist in the event then they should default to 50 as per the spec
const { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels;
- const me = room.getMember(cli.getUserId());
+ const me = room.getMember(cli.getUserId() || "");
if (!me) {
// we aren't in the room, so return no admin tooling
return ;
@@ -879,7 +891,7 @@ const RoomAdminToolsContainer: React.FC = ({
);
}
- if (!isMe && canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
+ if (!isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom()) {
muteButton = (
{
setSelectedPowerLevel(powerLevel);
- const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
- return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
+ const applyPowerChange = (
+ roomId: string,
+ target: string,
+ powerLevel: number,
+ powerLevelEvent: MatrixEvent,
+ ) => {
+ return cli.setPowerLevel(roomId, target, powerLevel, powerLevelEvent).then(
function () {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
@@ -1046,7 +1063,7 @@ const PowerLevelEditor: React.FC<{
if (!powerLevelEvent) return;
const myUserId = cli.getUserId();
- const myPower = powerLevelEvent.getContent().users[myUserId];
+ const myPower = powerLevelEvent.getContent().users[myUserId || ""];
if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) {
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("Warning!"),
@@ -1085,7 +1102,7 @@ const PowerLevelEditor: React.FC<{
return (
{
const cli = useContext(MatrixClientContext);
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
- const [devices, setDevices] = useState(undefined);
+ const [devices, setDevices] = useState(undefined);
// Download device lists
useEffect(() => {
setDevices(undefined);
@@ -1116,8 +1133,8 @@ export const useDevices = (userId: string) => {
return;
}
- disambiguateDevices(devices);
- setDevices(devices);
+ disambiguateDevices(devices as IDevice[]);
+ setDevices(devices as IDevice[]);
} catch (err) {
setDevices(null);
}
@@ -1136,17 +1153,17 @@ export const useDevices = (userId: string) => {
const updateDevices = async () => {
const newDevices = cli.getStoredDevicesForUser(userId);
if (cancel) return;
- setDevices(newDevices);
+ setDevices(newDevices as IDevice[]);
};
- const onDevicesUpdated = (users) => {
+ const onDevicesUpdated = (users: string[]) => {
if (!users.includes(userId)) return;
updateDevices();
};
- const onDeviceVerificationChanged = (_userId, device) => {
+ const onDeviceVerificationChanged = (_userId: string, deviceId: string) => {
if (_userId !== userId) return;
updateDevices();
};
- const onUserTrustStatusChanged = (_userId, trustStatus) => {
+ const onUserTrustStatusChanged = (_userId: string, trustLevel: UserTrustLevel) => {
if (_userId !== userId) return;
updateDevices();
};
@@ -1229,9 +1246,11 @@ const BasicUserInfo: React.FC<{
logger.error("Failed to deactivate user");
logger.error(err);
+ const description = err instanceof Error ? err.message : _t("Operation failed");
+
Modal.createDialog(ErrorDialog, {
title: _t("Failed to deactivate user"),
- description: err && err.message ? err.message : _t("Operation failed"),
+ description,
});
}
}, [cli, member.userId]);
@@ -1317,12 +1336,12 @@ const BasicUserInfo: React.FC<{
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId);
- const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified();
+ const userVerified = cryptoEnabled && userTrust && userTrust.isCrossSigningVerified();
const isMe = member.userId === cli.getUserId();
const canVerify =
cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe && devices && devices.length > 0;
- const setUpdating = (updating) => {
+ const setUpdating: SetUpdating = (updating) => {
setPendingUpdateCount((count) => count + (updating ? 1 : -1));
};
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating);
@@ -1408,9 +1427,9 @@ const BasicUserInfo: React.FC<{
export type Member = User | RoomMember;
-const UserInfoHeader: React.FC<{
+export const UserInfoHeader: React.FC<{
member: Member;
- e2eStatus: E2EStatus;
+ e2eStatus?: E2EStatus;
roomId?: string;
}> = ({ member, e2eStatus, roomId }) => {
const cli = useContext(MatrixClientContext);
@@ -1427,9 +1446,11 @@ const UserInfoHeader: React.FC<{
name: (member as RoomMember).name || (member as User).displayName,
};
- Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
+ Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
}, [member]);
+ const avatarUrl = (member as User).avatarUrl;
+
const avatarElement = (
@@ -1442,7 +1463,7 @@ const UserInfoHeader: React.FC<{
resizeMethod="scale"
fallbackUserId={member.userId}
onClick={onMemberAvatarClick}
- urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined}
+ urls={avatarUrl ? [avatarUrl] : undefined}
/>
@@ -1475,10 +1496,7 @@ const UserInfoHeader: React.FC<{
);
}
- let e2eIcon;
- if (e2eStatus) {
- e2eIcon = ;
- }
+ const e2eIcon = e2eStatus ? : null;
const displayName = (member as RoomMember).rawDisplayName;
return (
@@ -1496,7 +1514,7 @@ const UserInfoHeader: React.FC<{
- {UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, {
+ {UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
roomId,
withDisplayName: true,
})}
@@ -1533,7 +1551,7 @@ const UserInfo: React.FC
= ({ user, room, onClose, phase = RightPanelPha
const classes = ["mx_UserInfo"];
- let cardState: IRightPanelCardState;
+ let cardState: IRightPanelCardState = {};
// We have no previousPhase for when viewing a UserInfo without a Room at this time
if (room && phase === RightPanelPhases.EncryptionPanel) {
cardState = { member };
@@ -1551,10 +1569,10 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha
case RightPanelPhases.SpaceMemberInfo:
content = (
);
break;
@@ -1565,7 +1583,7 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha
{...(props as React.ComponentProps)}
member={member as User | RoomMember}
onClose={onEncryptionPanelClose}
- isRoomEncrypted={isRoomEncrypted}
+ isRoomEncrypted={Boolean(isRoomEncrypted)}
/>
);
break;
@@ -1582,7 +1600,7 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha
let scopeHeader;
if (room?.isSpaceRoom()) {
scopeHeader = (
-
+
diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx
index d7e9a5f71a..799fc98d64 100644
--- a/src/components/views/rooms/MessageComposer.tsx
+++ b/src/components/views/rooms/MessageComposer.tsx
@@ -58,6 +58,8 @@ import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysi
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
import { SdkContextClass } from "../../../contexts/SDKContext";
+import { VoiceBroadcastInfoState } from "../../../voice-broadcast";
+import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStartVoiceMessageBroadcastDialog";
let instanceCount = 0;
@@ -445,6 +447,20 @@ export class MessageComposer extends React.Component
{
}
}
+ private onRecordStartEndClick = (): void => {
+ const currentBroadcastRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent();
+
+ if (currentBroadcastRecording && currentBroadcastRecording.getState() !== VoiceBroadcastInfoState.Stopped) {
+ createCantStartVoiceMessageBroadcastDialog();
+ } else {
+ this.voiceRecordingButton.current?.onRecordStartEndClick();
+ }
+
+ if (this.context.narrow) {
+ this.toggleButtonMenu();
+ }
+ };
+
public render() {
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus);
const e2eIcon = hasE2EIcon && (
@@ -588,12 +604,7 @@ export class MessageComposer extends React.Component {
isStickerPickerOpen={this.state.isStickerPickerOpen}
menuPosition={menuPosition}
relation={this.props.relation}
- onRecordStartEndClick={() => {
- this.voiceRecordingButton.current?.onRecordStartEndClick();
- if (this.context.narrow) {
- this.toggleButtonMenu();
- }
- }}
+ onRecordStartEndClick={this.onRecordStartEndClick}
setStickerPickerOpen={this.setStickerPickerOpen}
showLocationButton={!window.electron}
showPollsButton={this.state.showPollsButton}
diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx
index 2285733da9..bce1182a13 100644
--- a/src/components/views/rooms/MessageComposerButtons.tsx
+++ b/src/components/views/rooms/MessageComposerButtons.tsx
@@ -376,8 +376,8 @@ function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonP
openLinkModal(composer, composerContext)}
+ onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
icon={}
/>
diff --git a/src/components/views/rooms/wysiwyg_composer/components/LinkModal.tsx b/src/components/views/rooms/wysiwyg_composer/components/LinkModal.tsx
index 2dcfc43ead..0e41d8baa4 100644
--- a/src/components/views/rooms/wysiwyg_composer/components/LinkModal.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/components/LinkModal.tsx
@@ -17,17 +17,28 @@ limitations under the License.
import { FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
import React, { ChangeEvent, useState } from "react";
-import { _td } from "../../../../../languageHandler";
+import { _t } from "../../../../../languageHandler";
import Modal from "../../../../../Modal";
-import QuestionDialog from "../../../dialogs/QuestionDialog";
import Field from "../../../elements/Field";
import { ComposerContextState } from "../ComposerContext";
import { isSelectionEmpty, setSelection } from "../utils/selection";
+import BaseDialog from "../../../dialogs/BaseDialog";
+import DialogButtons from "../../../elements/DialogButtons";
-export function openLinkModal(composer: FormattingFunctions, composerContext: ComposerContextState) {
+export function openLinkModal(
+ composer: FormattingFunctions,
+ composerContext: ComposerContextState,
+ isEditing: boolean,
+) {
const modal = Modal.createDialog(
LinkModal,
- { composerContext, composer, onClose: () => modal.close(), isTextEnabled: isSelectionEmpty() },
+ {
+ composerContext,
+ composer,
+ onClose: () => modal.close(),
+ isTextEnabled: isSelectionEmpty(),
+ isEditing,
+ },
"mx_CompoundDialog",
false,
true,
@@ -43,48 +54,86 @@ interface LinkModalProps {
isTextEnabled: boolean;
onClose: () => void;
composerContext: ComposerContextState;
+ isEditing: boolean;
}
-export function LinkModal({ composer, isTextEnabled, onClose, composerContext }: LinkModalProps) {
- const [fields, setFields] = useState({ text: "", link: "" });
- const isSaveDisabled = (isTextEnabled && isEmpty(fields.text)) || isEmpty(fields.link);
+export function LinkModal({ composer, isTextEnabled, onClose, composerContext, isEditing }: LinkModalProps) {
+ const [hasLinkChanged, setHasLinkChanged] = useState(false);
+ const [fields, setFields] = useState({ text: "", link: isEditing ? composer.getLink() : "" });
+ const hasText = !isEditing && isTextEnabled;
+ const isSaveDisabled = !hasLinkChanged || (hasText && isEmpty(fields.text)) || isEmpty(fields.link);
return (
- {
- if (isClickOnSave) {
+ title={isEditing ? _t("Edit link") : _t("Create a link")}
+ hasCancel={true}
+ onFinished={onClose}
+ >
+