Merge branch 'develop' into gsouquet/threads-forceenablelabsflag
This commit is contained in:
commit
d4f247d1fe
97 changed files with 3280 additions and 1325 deletions
|
@ -20,15 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import {
|
||||
M_EMOTE,
|
||||
M_NOTICE,
|
||||
M_MESSAGE,
|
||||
MessageEvent,
|
||||
M_POLL_START,
|
||||
M_POLL_END,
|
||||
PollStartEvent,
|
||||
} from "matrix-events-sdk";
|
||||
import { M_POLL_START, M_POLL_END, PollStartEvent } from "matrix-events-sdk";
|
||||
|
||||
import { _t } from "./languageHandler";
|
||||
import * as Roles from "./Roles";
|
||||
|
@ -347,17 +339,6 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null {
|
|||
message = textForRedactedPollAndMessageEvent(ev);
|
||||
}
|
||||
|
||||
if (SettingsStore.isEnabled("feature_extensible_events")) {
|
||||
const extev = ev.unstableExtensibleEvent as MessageEvent;
|
||||
if (extev) {
|
||||
if (extev.isEquivalentTo(M_EMOTE)) {
|
||||
return `* ${senderDisplayName} ${extev.text}`;
|
||||
} else if (extev.isEquivalentTo(M_NOTICE) || extev.isEquivalentTo(M_MESSAGE)) {
|
||||
return `${senderDisplayName}: ${extev.text}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.getContent().msgtype === MsgType.Emote) {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === MsgType.Image) {
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
|
||||
|
@ -59,35 +60,39 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
for (const timeline of [room, ...room.getThreads()]) {
|
||||
// If the current timeline has unread messages, we're done.
|
||||
if (doesRoomOrThreadHaveUnreadMessages(timeline)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// If we got here then no timelines were found with unread messages.
|
||||
return false;
|
||||
}
|
||||
|
||||
export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread): boolean {
|
||||
// If there are no messages yet in the timeline then it isn't fully initialised
|
||||
// and cannot be unread.
|
||||
if (!roomOrThread || roomOrThread.timeline.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
|
||||
// as we don't send RRs for our own messages, make sure we special case that
|
||||
// if *we* sent the last message into the room, we consider it not unread!
|
||||
// Should fix: https://github.com/vector-im/element-web/issues/3263
|
||||
// https://github.com/vector-im/element-web/issues/2427
|
||||
// ...and possibly some of the others at
|
||||
// https://github.com/vector-im/element-web/issues/3363
|
||||
if (roomOrThread.timeline.at(-1)?.getSender() === myUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the most recent read receipt sent by our account.
|
||||
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
||||
// despite the name of the method :((
|
||||
const readUpToId = room.getEventReadUpTo(myUserId!);
|
||||
|
||||
if (!SettingsStore.getValue("feature_threadenabled")) {
|
||||
// as we don't send RRs for our own messages, make sure we special case that
|
||||
// if *we* sent the last message into the room, we consider it not unread!
|
||||
// Should fix: https://github.com/vector-im/element-web/issues/3263
|
||||
// https://github.com/vector-im/element-web/issues/2427
|
||||
// ...and possibly some of the others at
|
||||
// https://github.com/vector-im/element-web/issues/3363
|
||||
if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// if the read receipt relates to an event is that part of a thread
|
||||
// we consider that there are no unread messages
|
||||
// This might be a false negative, but probably the best we can do until
|
||||
// the read receipts have evolved to cater for threads
|
||||
if (readUpToId) {
|
||||
const event = room.findEventById(readUpToId);
|
||||
if (event?.getThread()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const readUpToId = roomOrThread.getEventReadUpTo(myUserId!);
|
||||
|
||||
// this just looks at whatever history we have, which if we've only just started
|
||||
// up probably won't be very much, so if the last couple of events are ones that
|
||||
|
@ -96,8 +101,8 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
|||
// but currently we just guess.
|
||||
|
||||
// Loop through messages, starting with the most recent...
|
||||
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = room.timeline[i];
|
||||
for (let i = roomOrThread.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = roomOrThread.timeline[i];
|
||||
|
||||
if (ev.getId() == readUpToId) {
|
||||
// If we've read up to this event, there's nothing more recent
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -15,12 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { NotificationCount, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { getUnsentMessages } from "../components/structures/RoomStatusBar";
|
||||
import { getRoomNotifsState, getUnreadNotificationCount, RoomNotifState } from "../RoomNotifs";
|
||||
import { NotificationColor } from "../stores/notifications/NotificationColor";
|
||||
import { doesRoomHaveUnreadMessages } from "../Unread";
|
||||
import { doesRoomOrThreadHaveUnreadMessages } from "../Unread";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
|
||||
import { useEventEmitter } from "./useEventEmitter";
|
||||
|
||||
|
@ -75,12 +76,14 @@ export const useUnreadNotifications = (
|
|||
setColor(NotificationColor.Red);
|
||||
} else if (greyNotifs > 0) {
|
||||
setColor(NotificationColor.Grey);
|
||||
} else if (!threadId) {
|
||||
// TODO: No support for `Bold` on threads at the moment
|
||||
|
||||
} else {
|
||||
// We don't have any notified messages, but we might have unread messages. Let's
|
||||
// find out.
|
||||
const hasUnread = doesRoomHaveUnreadMessages(room);
|
||||
let roomOrThread: Room | Thread = room;
|
||||
if (threadId) {
|
||||
roomOrThread = room.getThread(threadId)!;
|
||||
}
|
||||
const hasUnread = doesRoomOrThreadHaveUnreadMessages(roomOrThread);
|
||||
setColor(hasUnread ? NotificationColor.Bold : NotificationColor.None);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -650,6 +650,8 @@
|
|||
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.",
|
||||
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.",
|
||||
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.",
|
||||
"Connection error": "Connection error",
|
||||
"Unfortunately we're unable to start a recording right now. Please try again later.": "Unfortunately we're unable to start a recording right now. Please try again later.",
|
||||
"Can’t start a call": "Can’t start a call",
|
||||
"You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.",
|
||||
"You ended a <a>voice broadcast</a>": "You ended a <a>voice broadcast</a>",
|
||||
|
@ -936,7 +938,6 @@
|
|||
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
|
||||
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
|
||||
"Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
|
||||
"Show extensible event representation of events": "Show extensible event representation of events",
|
||||
"Show current avatar and name for users in message history": "Show current avatar and name for users in message history",
|
||||
"Show HTML representation of room topics": "Show HTML representation of room topics",
|
||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||
|
@ -2135,6 +2136,7 @@
|
|||
"Underline": "Underline",
|
||||
"Code": "Code",
|
||||
"Link": "Link",
|
||||
"Edit link": "Edit link",
|
||||
"Create a link": "Create a link",
|
||||
"Text": "Text",
|
||||
"Message Actions": "Message Actions",
|
||||
|
@ -2686,6 +2688,8 @@
|
|||
"Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)",
|
||||
"Remove %(count)s messages|other": "Remove %(count)s messages",
|
||||
"Remove %(count)s messages|one": "Remove 1 message",
|
||||
"Can't start voice message": "Can't start voice message",
|
||||
"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.": "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.",
|
||||
"Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s",
|
||||
"Unavailable": "Unavailable",
|
||||
"Changelog": "Changelog",
|
||||
|
|
|
@ -340,13 +340,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
|||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"feature_extensible_events": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Developer, // developer for now, eventually Messaging and default on
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td("Show extensible event representation of events"),
|
||||
default: false,
|
||||
},
|
||||
"useOnlyCurrentProfiles": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td("Show current avatar and name for users in message history"),
|
||||
|
|
|
@ -40,8 +40,8 @@ const formatUrl = (): string | undefined => {
|
|||
].join("");
|
||||
};
|
||||
|
||||
export const getClientInformationEventType = (deviceId: string): string =>
|
||||
`io.element.matrix_client_information.${deviceId}`;
|
||||
const clientInformationEventPrefix = "io.element.matrix_client_information.";
|
||||
export const getClientInformationEventType = (deviceId: string): string => `${clientInformationEventPrefix}${deviceId}`;
|
||||
|
||||
/**
|
||||
* Record extra client information for the current device
|
||||
|
@ -52,7 +52,7 @@ export const recordClientInformation = async (
|
|||
sdkConfig: IConfigOptions,
|
||||
platform: BasePlatform,
|
||||
): Promise<void> => {
|
||||
const deviceId = matrixClient.getDeviceId();
|
||||
const deviceId = matrixClient.getDeviceId()!;
|
||||
const { brand } = sdkConfig;
|
||||
const version = await platform.getAppVersion();
|
||||
const type = getClientInformationEventType(deviceId);
|
||||
|
@ -66,12 +66,27 @@ export const recordClientInformation = async (
|
|||
};
|
||||
|
||||
/**
|
||||
* Remove extra client information
|
||||
* @todo(kerrya) revisit after MSC3391: account data deletion is done
|
||||
* (PSBE-12)
|
||||
* Remove client information events for devices that no longer exist
|
||||
* @param validDeviceIds - ids of current devices,
|
||||
* client information for devices NOT in this list will be removed
|
||||
*/
|
||||
export const pruneClientInformation = (validDeviceIds: string[], matrixClient: MatrixClient): void => {
|
||||
Object.values(matrixClient.store.accountData).forEach((event) => {
|
||||
if (!event.getType().startsWith(clientInformationEventPrefix)) {
|
||||
return;
|
||||
}
|
||||
const [, deviceId] = event.getType().split(clientInformationEventPrefix);
|
||||
if (deviceId && !validDeviceIds.includes(deviceId)) {
|
||||
matrixClient.deleteAccountData(event.getType());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove extra client information for current device
|
||||
*/
|
||||
export const removeClientInformation = async (matrixClient: MatrixClient): Promise<void> => {
|
||||
const deviceId = matrixClient.getDeviceId();
|
||||
const deviceId = matrixClient.getDeviceId()!;
|
||||
const type = getClientInformationEventType(deviceId);
|
||||
const clientInformation = getDeviceClientInformation(matrixClient, deviceId);
|
||||
|
||||
|
|
|
@ -60,13 +60,20 @@ export class VoiceBroadcastRecording
|
|||
{
|
||||
private state: VoiceBroadcastInfoState;
|
||||
private recorder: VoiceBroadcastRecorder;
|
||||
private sequence = 1;
|
||||
private dispatcherRef: string;
|
||||
private chunkEvents = new VoiceBroadcastChunkEvents();
|
||||
private chunkRelationHelper: RelationsHelper;
|
||||
private maxLength: number;
|
||||
private timeLeft: number;
|
||||
|
||||
/**
|
||||
* Broadcast chunks have a sequence number to bring them in the correct order and to know if a message is missing.
|
||||
* This variable holds the last sequence number.
|
||||
* Starts with 0 because there is no chunk at the beginning of a broadcast.
|
||||
* Will be incremented when a chunk message is created.
|
||||
*/
|
||||
private sequence = 0;
|
||||
|
||||
public constructor(
|
||||
public readonly infoEvent: MatrixEvent,
|
||||
private client: MatrixClient,
|
||||
|
@ -268,7 +275,8 @@ export class VoiceBroadcastRecording
|
|||
event_id: this.infoEvent.getId(),
|
||||
};
|
||||
content["io.element.voice_broadcast_chunk"] = {
|
||||
sequence: this.sequence++,
|
||||
/** Increment the last sequence number and use it for this message. Also see {@link sequence}. */
|
||||
sequence: ++this.sequence,
|
||||
};
|
||||
|
||||
await this.client.sendMessage(this.infoEvent.getRoomId(), content);
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||
|
||||
import { hasRoomLiveVoiceBroadcast, VoiceBroadcastInfoEventType, VoiceBroadcastRecordingsStore } from "..";
|
||||
import InfoDialog from "../../components/views/dialogs/InfoDialog";
|
||||
|
@ -67,6 +68,14 @@ const showOthersAlreadyRecordingDialog = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const showNoConnectionDialog = (): void => {
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("Connection error"),
|
||||
description: <p>{_t("Unfortunately we're unable to start a recording right now. Please try again later.")}</p>,
|
||||
hasCloseButton: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const checkVoiceBroadcastPreConditions = async (
|
||||
room: Room,
|
||||
client: MatrixClient,
|
||||
|
@ -86,6 +95,11 @@ export const checkVoiceBroadcastPreConditions = async (
|
|||
return false;
|
||||
}
|
||||
|
||||
if (client.getSyncState() === SyncState.Error) {
|
||||
showNoConnectionDialog();
|
||||
return false;
|
||||
}
|
||||
|
||||
const { hasBroadcast, startedByUser } = await hasRoomLiveVoiceBroadcast(client, room, currentUserId);
|
||||
|
||||
if (hasBroadcast && startedByUser) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue