Merge branch 'develop' into gsouquet/threads-forceenablelabsflag
This commit is contained in:
commit
d4f247d1fe
97 changed files with 3280 additions and 1325 deletions
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2022 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 { _t } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
|
||||
export const createCantStartVoiceMessageBroadcastDialog = (): void => {
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("Can't start voice message"),
|
||||
description: (
|
||||
<p>
|
||||
{_t(
|
||||
"You can't start a voice message as you are currently recording a live broadcast. " +
|
||||
"Please end your live broadcast in order to start recording a voice message.",
|
||||
)}
|
||||
</p>
|
||||
),
|
||||
hasCloseButton: true,
|
||||
});
|
||||
};
|
|
@ -96,7 +96,7 @@ export default abstract class ScrollableBaseModal<
|
|||
aria-label={_t("Close dialog")}
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<form onSubmit={this.onSubmit} className="mx_CompoundDialog_form">
|
||||
<div className="mx_CompoundDialog_content">{this.renderContent()}</div>
|
||||
<div className="mx_CompoundDialog_footer">
|
||||
<AccessibleButton onClick={this.onCancel} kind="primary_outline">
|
||||
|
|
|
@ -262,7 +262,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
|
||||
this.inputRef = inputRef || React.createRef();
|
||||
|
||||
inputProps.placeholder = inputProps.placeholder || inputProps.label;
|
||||
inputProps.placeholder = inputProps.placeholder ?? inputProps.label;
|
||||
inputProps.id = this.id; // this overwrites the id from props
|
||||
|
||||
inputProps.onFocus = this.onFocus;
|
||||
|
|
|
@ -18,7 +18,6 @@ import React, { createRef, SyntheticEvent, MouseEvent, ReactNode } from "react";
|
|||
import ReactDOM from "react-dom";
|
||||
import highlight from "highlight.js";
|
||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import { isEventLike, LegacyMsgType, M_MESSAGE, MessageEvent } from "matrix-events-sdk";
|
||||
|
||||
import * as HtmlUtils from "../../../HtmlUtils";
|
||||
import { formatDate } from "../../../DateUtils";
|
||||
|
@ -579,29 +578,6 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
// only strip reply if this is the original replying event, edits thereafter do not have the fallback
|
||||
const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent);
|
||||
let body: ReactNode;
|
||||
if (SettingsStore.isEnabled("feature_extensible_events")) {
|
||||
const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent;
|
||||
if (extev?.isEquivalentTo(M_MESSAGE)) {
|
||||
isEmote = isEventLike(extev.wireFormat, LegacyMsgType.Emote);
|
||||
isNotice = isEventLike(extev.wireFormat, LegacyMsgType.Notice);
|
||||
body = HtmlUtils.bodyToHtml(
|
||||
{
|
||||
body: extev.text,
|
||||
format: extev.html ? "org.matrix.custom.html" : undefined,
|
||||
formatted_body: extev.html,
|
||||
msgtype: MsgType.Text,
|
||||
},
|
||||
this.props.highlights,
|
||||
{
|
||||
disableBigEmoji: isEmote || !SettingsStore.getValue<boolean>("TextualBody.enableBigEmoji"),
|
||||
// Part of Replies fallback support
|
||||
stripReplyFallback: stripReply,
|
||||
ref: this.contentRef,
|
||||
returnString: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!body) {
|
||||
isEmote = content.msgtype === MsgType.Emote;
|
||||
isNotice = content.msgtype === MsgType.Notice;
|
||||
|
|
|
@ -69,7 +69,7 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
|||
if (onClose) {
|
||||
closeButton = (
|
||||
<AccessibleButton
|
||||
data-test-id="base-card-close-button"
|
||||
data-testid="base-card-close-button"
|
||||
className="mx_BaseCard_close"
|
||||
onClick={onClose}
|
||||
title={closeLabel || _t("Close")}
|
||||
|
|
|
@ -21,6 +21,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { ThreadEvent } from "matrix-js-sdk/src/models/thread";
|
||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -44,6 +45,7 @@ import { NotificationStateEvents } from "../../../stores/notifications/Notificat
|
|||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread";
|
||||
|
||||
const ROOM_INFO_PHASES = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
|
@ -154,7 +156,17 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
if (!this.supportsThreadNotifications) {
|
||||
this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
} else {
|
||||
// Notification badge may change if the notification counts from the
|
||||
// server change, if a new thread is created or updated, or if a
|
||||
// receipt is sent in the thread.
|
||||
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||
this.props.room?.on(RoomEvent.Receipt, this.onNotificationUpdate);
|
||||
this.props.room?.on(RoomEvent.Timeline, this.onNotificationUpdate);
|
||||
this.props.room?.on(RoomEvent.Redaction, this.onNotificationUpdate);
|
||||
this.props.room?.on(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
|
||||
this.props.room?.on(RoomEvent.MyMembership, this.onNotificationUpdate);
|
||||
this.props.room?.on(ThreadEvent.New, this.onNotificationUpdate);
|
||||
this.props.room?.on(ThreadEvent.Update, this.onNotificationUpdate);
|
||||
}
|
||||
this.onNotificationUpdate();
|
||||
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||
|
@ -166,6 +178,13 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
this.threadNotificationState?.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
} else {
|
||||
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||
this.props.room?.off(RoomEvent.Receipt, this.onNotificationUpdate);
|
||||
this.props.room?.off(RoomEvent.Timeline, this.onNotificationUpdate);
|
||||
this.props.room?.off(RoomEvent.Redaction, this.onNotificationUpdate);
|
||||
this.props.room?.off(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
|
||||
this.props.room?.off(RoomEvent.MyMembership, this.onNotificationUpdate);
|
||||
this.props.room?.off(ThreadEvent.New, this.onNotificationUpdate);
|
||||
this.props.room?.off(ThreadEvent.Update, this.onNotificationUpdate);
|
||||
}
|
||||
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||
}
|
||||
|
@ -191,9 +210,17 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
return NotificationColor.Red;
|
||||
case NotificationCountType.Total:
|
||||
return NotificationColor.Grey;
|
||||
default:
|
||||
return NotificationColor.None;
|
||||
}
|
||||
// We don't have any notified messages, but we might have unread messages. Let's
|
||||
// find out.
|
||||
for (const thread of this.props.room!.getThreads()) {
|
||||
// If the current thread has unread messages, we're done.
|
||||
if (doesRoomOrThreadHaveUnreadMessages(thread)) {
|
||||
return NotificationColor.Bold;
|
||||
}
|
||||
}
|
||||
// Otherwise, no notification color.
|
||||
return NotificationColor.None;
|
||||
}
|
||||
|
||||
private onUpdateStatus = (notificationState: SummarizedNotificationState): void => {
|
||||
|
|
|
@ -29,6 +29,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import Modal from "../../../Modal";
|
||||
|
@ -84,7 +85,7 @@ export interface IDevice {
|
|||
getDisplayName(): string;
|
||||
}
|
||||
|
||||
const disambiguateDevices = (devices: IDevice[]) => {
|
||||
export const disambiguateDevices = (devices: IDevice[]) => {
|
||||
const names = Object.create(null);
|
||||
for (let i = 0; i < devices.length; i++) {
|
||||
const name = devices[i].getDisplayName();
|
||||
|
@ -94,7 +95,7 @@ const disambiguateDevices = (devices: IDevice[]) => {
|
|||
}
|
||||
for (const name in names) {
|
||||
if (names[name].length > 1) {
|
||||
names[name].forEach((j) => {
|
||||
names[name].forEach((j: number) => {
|
||||
devices[j].ambiguous = true;
|
||||
});
|
||||
}
|
||||
|
@ -149,7 +150,7 @@ function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: bool
|
|||
}, [cli, member, canVerify]);
|
||||
}
|
||||
|
||||
function DeviceItem({ userId, device }: { userId: string; device: IDevice }) {
|
||||
export function DeviceItem({ userId, device }: { userId: string; device: IDevice }) {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const isMe = userId === cli.getUserId();
|
||||
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
|
||||
|
@ -172,7 +173,10 @@ function DeviceItem({ userId, device }: { userId: string; device: IDevice }) {
|
|||
});
|
||||
|
||||
const onDeviceClick = () => {
|
||||
verifyDevice(cli.getUser(userId), device);
|
||||
const user = cli.getUser(userId);
|
||||
if (user) {
|
||||
verifyDevice(user, device);
|
||||
}
|
||||
};
|
||||
|
||||
let deviceName;
|
||||
|
@ -315,7 +319,7 @@ const MessageButton = ({ member }: { member: RoomMember }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const UserOptionsSection: React.FC<{
|
||||
export const UserOptionsSection: React.FC<{
|
||||
member: RoomMember;
|
||||
isIgnored: boolean;
|
||||
canInvite: boolean;
|
||||
|
@ -367,7 +371,8 @@ const UserOptionsSection: React.FC<{
|
|||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
highlighted: true,
|
||||
event_id: room.getEventReadUpTo(member.userId),
|
||||
// this could return null, the default prevents a type error
|
||||
event_id: room?.getEventReadUpTo(member.userId) || undefined,
|
||||
room_id: member.roomId,
|
||||
metricsTrigger: undefined, // room doesn't change
|
||||
});
|
||||
|
@ -402,16 +407,18 @@ const UserOptionsSection: React.FC<{
|
|||
const onInviteUserButton = async (ev: ButtonEvent) => {
|
||||
try {
|
||||
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
||||
const inviter = new MultiInviter(roomId);
|
||||
const inviter = new MultiInviter(roomId || "");
|
||||
await inviter.invite([member.userId]).then(() => {
|
||||
if (inviter.getCompletionState(member.userId) !== "invited") {
|
||||
throw new Error(inviter.getErrorText(member.userId));
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const description = err instanceof Error ? err.message : _t("Operation failed");
|
||||
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Failed to invite"),
|
||||
description: err && err.message ? err.message : _t("Operation failed"),
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -432,10 +439,7 @@ const UserOptionsSection: React.FC<{
|
|||
</AccessibleButton>
|
||||
);
|
||||
|
||||
let directMessageButton: JSX.Element;
|
||||
if (!isMe) {
|
||||
directMessageButton = <MessageButton member={member} />;
|
||||
}
|
||||
const directMessageButton = isMe ? null : <MessageButton member={member} />;
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
|
@ -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<IPowerLevelsContent>(getPowerLevels(room));
|
||||
|
@ -538,7 +550,7 @@ interface IBaseProps {
|
|||
stopUpdating(): void;
|
||||
}
|
||||
|
||||
const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||
export const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
// check if user can be kicked/disinvited
|
||||
|
@ -566,7 +578,7 @@ const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBas
|
|||
space: room,
|
||||
spaceChildFilter: (child: Room) => {
|
||||
// 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<IBaseProps> = ({ member }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||
export const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const isBanned = member.membership === "ban";
|
||||
|
@ -674,7 +686,7 @@ const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBa
|
|||
spaceChildFilter: isBanned
|
||||
? (child: Room) => {
|
||||
// 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<IBa
|
|||
}
|
||||
: (child: Room) => {
|
||||
// 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<IBaseRoomProps> = ({ member, room, powerLevels,
|
|||
);
|
||||
};
|
||||
|
||||
const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||
export const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||
room,
|
||||
children,
|
||||
member,
|
||||
|
@ -855,7 +867,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
// 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 <div />;
|
||||
|
@ -879,7 +891,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
<BanToggleButton room={room} member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
}
|
||||
if (!isMe && canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
|
||||
if (!isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom()) {
|
||||
muteButton = (
|
||||
<MuteToggleButton
|
||||
member={member}
|
||||
|
@ -949,7 +961,7 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IR
|
|||
const powerLevels = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
||||
if (!powerLevels) return;
|
||||
|
||||
const me = room.getMember(cli.getUserId());
|
||||
const me = room.getMember(cli.getUserId() || "");
|
||||
if (!me) return;
|
||||
|
||||
const them = user;
|
||||
|
@ -1006,7 +1018,7 @@ const PowerLevelSection: React.FC<{
|
|||
}
|
||||
};
|
||||
|
||||
const PowerLevelEditor: React.FC<{
|
||||
export const PowerLevelEditor: React.FC<{
|
||||
user: RoomMember;
|
||||
room: Room;
|
||||
roomPermissions: IRoomPermissions;
|
||||
|
@ -1022,8 +1034,13 @@ const PowerLevelEditor: React.FC<{
|
|||
async (powerLevel: number) => {
|
||||
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 (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<PowerSelector
|
||||
label={null}
|
||||
label={undefined}
|
||||
value={selectedPowerLevel}
|
||||
maxValue={roomPermissions.modifyLevelMax}
|
||||
usersDefault={powerLevelUsersDefault}
|
||||
|
@ -1099,7 +1116,7 @@ export const useDevices = (userId: string) => {
|
|||
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 | null | IDevice[]>(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 = (
|
||||
<div className="mx_UserInfo_avatar">
|
||||
<div className="mx_UserInfo_avatar_transition">
|
||||
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1475,10 +1496,7 @@ const UserInfoHeader: React.FC<{
|
|||
);
|
||||
}
|
||||
|
||||
let e2eIcon;
|
||||
if (e2eStatus) {
|
||||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||
}
|
||||
const e2eIcon = e2eStatus ? <E2EIcon size={18} status={e2eStatus} isUser={true} /> : null;
|
||||
|
||||
const displayName = (member as RoomMember).rawDisplayName;
|
||||
return (
|
||||
|
@ -1496,7 +1514,7 @@ const UserInfoHeader: React.FC<{
|
|||
</h2>
|
||||
</div>
|
||||
<div className="mx_UserInfo_profile_mxid">
|
||||
{UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, {
|
||||
{UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
|
||||
roomId,
|
||||
withDisplayName: true,
|
||||
})}
|
||||
|
@ -1533,7 +1551,7 @@ const UserInfo: React.FC<IProps> = ({ 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<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
|||
case RightPanelPhases.SpaceMemberInfo:
|
||||
content = (
|
||||
<BasicUserInfo
|
||||
room={room}
|
||||
room={room as Room}
|
||||
member={member as User}
|
||||
devices={devices}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
devices={devices as IDevice[]}
|
||||
isRoomEncrypted={Boolean(isRoomEncrypted)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -1565,7 +1583,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
|||
{...(props as React.ComponentProps<typeof EncryptionPanel>)}
|
||||
member={member as User | RoomMember}
|
||||
onClose={onEncryptionPanelClose}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
isRoomEncrypted={Boolean(isRoomEncrypted)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -1582,7 +1600,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
|||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
scopeHeader = (
|
||||
<div data-test-id="space-header" className="mx_RightPanel_scopeHeader">
|
||||
<div data-testid="space-header" className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
</div>
|
||||
|
|
|
@ -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<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
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<IProps, IState> {
|
|||
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}
|
||||
|
|
|
@ -376,8 +376,8 @@ function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonP
|
|||
<CollapsibleButton
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName={classNames({
|
||||
mx_MessageComposer_plain_text: isRichTextEnabled,
|
||||
mx_MessageComposer_rich_text: !isRichTextEnabled,
|
||||
mx_MessageComposer_plain_text: !isRichTextEnabled,
|
||||
mx_MessageComposer_rich_text: isRichTextEnabled,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
|
|
|
@ -120,7 +120,7 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP
|
|||
<Button
|
||||
isActive={actionStates.link === "reversed"}
|
||||
label={_td("Link")}
|
||||
onClick={() => openLinkModal(composer, composerContext)}
|
||||
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
|
||||
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<QuestionDialog
|
||||
<BaseDialog
|
||||
className="mx_LinkModal"
|
||||
title={_td("Create a link")}
|
||||
button={_td("Save")}
|
||||
buttonDisabled={isSaveDisabled}
|
||||
hasCancelButton={true}
|
||||
onFinished={async (isClickOnSave: boolean) => {
|
||||
if (isClickOnSave) {
|
||||
title={isEditing ? _t("Edit link") : _t("Create a link")}
|
||||
hasCancel={true}
|
||||
onFinished={onClose}
|
||||
>
|
||||
<form
|
||||
className="mx_LinkModal_content"
|
||||
onSubmit={async (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
onClose();
|
||||
|
||||
// When submitting is done when pressing enter when the link field has the focus,
|
||||
// The link field is getting back the focus (due to react-focus-lock)
|
||||
// So we are waiting that the focus stuff is done to play with the composer selection
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
await setSelection(composerContext.selection);
|
||||
composer.link(fields.link, isTextEnabled ? fields.text : undefined);
|
||||
}
|
||||
onClose();
|
||||
}}
|
||||
description={
|
||||
<div className="mx_LinkModal_content">
|
||||
{isTextEnabled && (
|
||||
<Field
|
||||
autoFocus={true}
|
||||
label={_td("Text")}
|
||||
value={fields.text}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setFields((fields) => ({ ...fields, text: e.target.value }))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
}}
|
||||
>
|
||||
{hasText && (
|
||||
<Field
|
||||
autoFocus={!isTextEnabled}
|
||||
label={_td("Link")}
|
||||
value={fields.link}
|
||||
required={true}
|
||||
autoFocus={true}
|
||||
label={_t("Text")}
|
||||
value={fields.text}
|
||||
className="mx_LinkModal_Field"
|
||||
placeholder=""
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setFields((fields) => ({ ...fields, link: e.target.value }))
|
||||
setFields((fields) => ({ ...fields, text: e.target.value }))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Field
|
||||
required={true}
|
||||
autoFocus={!hasText}
|
||||
label={_t("Link")}
|
||||
value={fields.link}
|
||||
className="mx_LinkModal_Field"
|
||||
placeholder=""
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFields((fields) => ({ ...fields, link: e.target.value }));
|
||||
setHasLinkChanged(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mx_LinkModal_buttons">
|
||||
{isEditing && (
|
||||
<button
|
||||
type="button"
|
||||
className="danger"
|
||||
onClick={() => {
|
||||
composer.removeLinks();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{_t("Remove")}
|
||||
</button>
|
||||
)}
|
||||
<DialogButtons
|
||||
primaryButton={_t("Save")}
|
||||
primaryDisabled={isSaveDisabled}
|
||||
primaryIsSubmit={true}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
|||
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { getDeviceClientInformation } from "../../../../utils/device/clientInformation";
|
||||
import { getDeviceClientInformation, pruneClientInformation } from "../../../../utils/device/clientInformation";
|
||||
import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types";
|
||||
import { useEventEmitter } from "../../../../hooks/useEventEmitter";
|
||||
import { parseUserAgent } from "../../../../utils/device/parseUserAgent";
|
||||
|
@ -116,8 +116,8 @@ export type DevicesState = {
|
|||
export const useOwnDevices = (): DevicesState => {
|
||||
const matrixClient = useContext(MatrixClientContext);
|
||||
|
||||
const currentDeviceId = matrixClient.getDeviceId();
|
||||
const userId = matrixClient.getUserId();
|
||||
const currentDeviceId = matrixClient.getDeviceId()!;
|
||||
const userId = matrixClient.getSafeUserId();
|
||||
|
||||
const [devices, setDevices] = useState<DevicesState["devices"]>({});
|
||||
const [pushers, setPushers] = useState<DevicesState["pushers"]>([]);
|
||||
|
@ -138,11 +138,6 @@ export const useOwnDevices = (): DevicesState => {
|
|||
const refreshDevices = useCallback(async () => {
|
||||
setIsLoadingDeviceList(true);
|
||||
try {
|
||||
// realistically we should never hit this
|
||||
// but it satisfies types
|
||||
if (!userId) {
|
||||
throw new Error("Cannot fetch devices without user id");
|
||||
}
|
||||
const devices = await fetchDevicesWithVerification(matrixClient, userId);
|
||||
setDevices(devices);
|
||||
|
||||
|
@ -176,6 +171,15 @@ export const useOwnDevices = (): DevicesState => {
|
|||
refreshDevices();
|
||||
}, [refreshDevices]);
|
||||
|
||||
useEffect(() => {
|
||||
const deviceIds = Object.keys(devices);
|
||||
// empty devices means devices have not been fetched yet
|
||||
// as there is always at least the current device
|
||||
if (deviceIds.length) {
|
||||
pruneClientInformation(deviceIds, matrixClient);
|
||||
}
|
||||
}, [devices, matrixClient]);
|
||||
|
||||
useEventEmitter(matrixClient, CryptoEvent.DevicesUpdated, (users: string[]): void => {
|
||||
if (users.includes(userId)) {
|
||||
refreshDevices();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue