mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-09 01:30:57 +00:00
Compare commits
5 Commits
robin/reve
...
hs/persist
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c19592af | ||
|
|
db2c5ce26c | ||
|
|
3fe5392588 | ||
|
|
a0044d6b5f | ||
|
|
68c03db557 |
@@ -128,7 +128,7 @@
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "1.11.0",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
|
||||
@@ -26,7 +26,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
&.mx_UserPill_me,
|
||||
&.mx_AtRoomPill {
|
||||
&.mx_AtRoomPill,
|
||||
&.mx_KeywordPill {
|
||||
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
||||
}
|
||||
|
||||
@@ -45,7 +46,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
/* We don't want to indicate clickability */
|
||||
&.mx_AtRoomPill:hover {
|
||||
&.mx_AtRoomPill:hover,
|
||||
&.mx_KeywordPill:hover {
|
||||
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
@@ -135,12 +135,6 @@ $left-gutter: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_EventTile_highlight,
|
||||
&.mx_EventTile_highlight .markdown-body,
|
||||
&.mx_EventTile_highlight .mx_EventTile_edited {
|
||||
color: $alert;
|
||||
}
|
||||
|
||||
&.mx_EventTile_bubbleContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 100px;
|
||||
|
||||
@@ -19,6 +19,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
}
|
||||
.mx_UserProfileSettings_profile_statusMessage {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserProfileSettings_profile_controls {
|
||||
|
||||
@@ -54,6 +54,7 @@ import { deop, op } from "./slash-commands/op";
|
||||
import { CommandCategories } from "./slash-commands/interface";
|
||||
import { Command } from "./slash-commands/command";
|
||||
import { goto, join } from "./slash-commands/join";
|
||||
import { SdkContextClass } from "./contexts/SDKContext";
|
||||
|
||||
export { CommandCategories, Command };
|
||||
|
||||
@@ -883,6 +884,25 @@ export const Commands = [
|
||||
},
|
||||
renderingTypes: [TimelineRenderingType.Room],
|
||||
}),
|
||||
new Command({
|
||||
command: "statusmsg",
|
||||
description: _td("slash_command|status_msg"),
|
||||
category: CommandCategories.actions,
|
||||
args: "<message>",
|
||||
isEnabled: (cli) => !isCurrentLocalRoom(cli),
|
||||
runFn: function (cli, _roomId, _threadId, args) {
|
||||
return success((async () => {
|
||||
const supported = await cli.doesServerSupportExtendedProfiles();
|
||||
if (!supported) return reject(new UserFriendlyError("slash_command|extended_profile_not_supported"));
|
||||
if (!args) return reject(new UserFriendlyError("slash_command|no_status_message"));
|
||||
await cli.setExtendedProfileProperty("uk.half-shot.status", args);
|
||||
// Refresh profile.
|
||||
void SdkContextClass.instance.userProfilesStore.fetchProfile(cli.getSafeUserId());
|
||||
})());
|
||||
},
|
||||
renderingTypes: [TimelineRenderingType.Room],
|
||||
}),
|
||||
|
||||
|
||||
// Command definitions for autocompletion ONLY:
|
||||
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
||||
|
||||
@@ -18,6 +18,7 @@ import { RoomMember } from "../../../../models/rooms/RoomMember";
|
||||
import { _t, _td, TranslationKey } from "../../../../languageHandler";
|
||||
import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier";
|
||||
import { E2EStatus } from "../../../../utils/ShieldUtils";
|
||||
import { useUserProfileValue } from "../../../../hooks/useUserProfileValue";
|
||||
|
||||
interface MemberTileViewModelProps {
|
||||
member: RoomMember;
|
||||
@@ -30,6 +31,7 @@ export interface MemberTileViewState extends MemberTileViewModelProps {
|
||||
onClick: () => void;
|
||||
title?: string;
|
||||
userLabel?: string;
|
||||
statusMessage?: string;
|
||||
}
|
||||
|
||||
export enum PowerStatus {
|
||||
@@ -44,10 +46,10 @@ const PowerLabel: Record<PowerStatus, TranslationKey> = {
|
||||
|
||||
export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberTileViewState {
|
||||
const [e2eStatus, setE2eStatus] = useState<E2EStatus | undefined>();
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const statusMessage = useUserProfileValue(cli, "uk.half-shot.status", props.member.userId);
|
||||
|
||||
useEffect(() => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
|
||||
const updateE2EStatus = async (): Promise<void> => {
|
||||
const { userId } = props.member;
|
||||
const isMe = userId === cli.getUserId();
|
||||
@@ -103,7 +105,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
|
||||
cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
|
||||
}
|
||||
};
|
||||
}, [props.member]);
|
||||
}, [props.member, cli]);
|
||||
|
||||
const onClick = (): void => {
|
||||
dis.dispatch({
|
||||
@@ -156,5 +158,6 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
|
||||
e2eStatus,
|
||||
showPresence: props.showPresence,
|
||||
userLabel,
|
||||
statusMessage: statusMessage ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export enum PillType {
|
||||
AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention
|
||||
EventInSameRoom = "TYPE_EVENT_IN_SAME_ROOM",
|
||||
EventInOtherRoom = "TYPE_EVENT_IN_OTHER_ROOM",
|
||||
Keyword = "TYPE_KEYWORD", // Used to highlight keywords that triggered a notification rule
|
||||
}
|
||||
|
||||
export const pillRoomNotifPos = (text: string | null): number => {
|
||||
@@ -76,14 +77,32 @@ export interface PillProps {
|
||||
room?: Room;
|
||||
// Whether to include an avatar in the pill
|
||||
shouldShowPillAvatar?: boolean;
|
||||
// Explicitly-provided text to display in the pill
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar = true }) => {
|
||||
const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({
|
||||
export const Pill: React.FC<PillProps> = ({
|
||||
type: propType,
|
||||
url,
|
||||
inMessage,
|
||||
room,
|
||||
shouldShowPillAvatar = true,
|
||||
text: customPillText,
|
||||
}) => {
|
||||
const {
|
||||
event,
|
||||
member,
|
||||
onClick,
|
||||
resourceId,
|
||||
targetRoom,
|
||||
text: linkText,
|
||||
type,
|
||||
} = usePermalink({
|
||||
room,
|
||||
type: propType,
|
||||
url,
|
||||
});
|
||||
const text = customPillText ?? linkText;
|
||||
|
||||
if (!type || !text) {
|
||||
return null;
|
||||
@@ -96,6 +115,7 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
|
||||
mx_UserPill: type === PillType.UserMention,
|
||||
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
|
||||
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
|
||||
mx_KeywordPill: type === PillType.Keyword,
|
||||
});
|
||||
|
||||
let avatar: ReactElement | null = null;
|
||||
@@ -131,6 +151,8 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
|
||||
case PillType.UserMention:
|
||||
avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
|
||||
break;
|
||||
case PillType.Keyword:
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -28,11 +28,12 @@ interface IProps {
|
||||
colored?: boolean;
|
||||
emphasizeDisplayName?: boolean;
|
||||
withTooltip?: boolean;
|
||||
statusMessage?: string;
|
||||
}
|
||||
|
||||
export default class DisambiguatedProfile extends React.Component<IProps> {
|
||||
public render(): React.ReactNode {
|
||||
const { fallbackName, member, colored, emphasizeDisplayName, withTooltip, onClick } = this.props;
|
||||
const { fallbackName, member, colored, emphasizeDisplayName, withTooltip, onClick, statusMessage } = this.props;
|
||||
const rawDisplayName = member?.rawDisplayName || fallbackName;
|
||||
const mxid = member?.userId;
|
||||
|
||||
@@ -42,6 +43,7 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
|
||||
}
|
||||
|
||||
let mxidElement;
|
||||
let statusElement;
|
||||
let title: string | undefined;
|
||||
|
||||
if (mxid) {
|
||||
@@ -59,6 +61,10 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
|
||||
});
|
||||
}
|
||||
|
||||
if (statusMessage) {
|
||||
statusElement = <span className="mx_DisambiguatedProfile_mxid">({statusMessage})</span>;
|
||||
}
|
||||
|
||||
const displayNameClasses = classNames(colorClass, {
|
||||
mx_DisambiguatedProfile_displayName: emphasizeDisplayName,
|
||||
});
|
||||
@@ -69,6 +75,7 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
|
||||
{rawDisplayName}
|
||||
</span>
|
||||
{mxidElement}
|
||||
{statusElement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import DisambiguatedProfile from "./DisambiguatedProfile";
|
||||
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
||||
import { useUserProfileValue } from "../../../hooks/useUserProfileValue";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
@@ -24,9 +26,11 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
|
||||
userId: mxEvent.getSender(),
|
||||
member: mxEvent.sender,
|
||||
});
|
||||
const statusMessage = useUserProfileValue(MatrixClientPeg.safeGet(), "uk.half-shot.status", mxEvent.sender?.userId);
|
||||
|
||||
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
|
||||
<DisambiguatedProfile
|
||||
statusMessage={statusMessage ?? undefined}
|
||||
fallbackName={mxEvent.getSender() ?? ""}
|
||||
onClick={onClick}
|
||||
member={member}
|
||||
|
||||
@@ -7,8 +7,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react";
|
||||
import { MsgType } from "matrix-js-sdk/src/matrix";
|
||||
import { MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { globToRegexp } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import * as HtmlUtils from "../../../HtmlUtils";
|
||||
import { formatDate } from "../../../DateUtils";
|
||||
@@ -35,6 +36,7 @@ import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
|
||||
import { IEventTileOps } from "../rooms/EventTile";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import CodeBlock from "./CodeBlock";
|
||||
import { Pill, PillType } from "../elements/Pill";
|
||||
import { ReactRootManager } from "../../../utils/react";
|
||||
|
||||
interface IState {
|
||||
@@ -100,6 +102,16 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight notification keywords using pills
|
||||
const pushDetails = this.props.mxEvent.getPushDetails();
|
||||
if (
|
||||
pushDetails.rule?.enabled &&
|
||||
pushDetails.rule.kind === PushRuleKind.ContentSpecific &&
|
||||
pushDetails.rule.pattern
|
||||
) {
|
||||
this.pillifyNotificationKeywords([content], this.regExpForKeywordPattern(pushDetails.rule.pattern));
|
||||
}
|
||||
}
|
||||
|
||||
private addCodeElement(pre: HTMLPreElement): void {
|
||||
@@ -210,6 +222,55 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the text that activated a push-notification keyword pattern.
|
||||
*/
|
||||
private pillifyNotificationKeywords(nodes: ArrayLike<Element>, exp: RegExp): void {
|
||||
let node: Node | null = nodes[0];
|
||||
while (node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.nodeValue;
|
||||
if (!text) {
|
||||
node = node.nextSibling;
|
||||
continue;
|
||||
}
|
||||
const match = text.match(exp);
|
||||
if (!match || match.length < 3) {
|
||||
node = node.nextSibling;
|
||||
continue;
|
||||
}
|
||||
const keywordText = match[2];
|
||||
const idx = match.index! + match[1].length;
|
||||
const before = text.substring(0, idx);
|
||||
const after = text.substring(idx + keywordText.length);
|
||||
|
||||
const container = document.createElement("span");
|
||||
const newContent = (
|
||||
<>
|
||||
{before}
|
||||
<TooltipProvider>
|
||||
<Pill text={keywordText} type={PillType.Keyword} />
|
||||
</TooltipProvider>
|
||||
{after}
|
||||
</>
|
||||
);
|
||||
this.reactRoots.render(newContent, container, node);
|
||||
|
||||
node.parentNode?.replaceChild(container, node);
|
||||
} else if (node.childNodes && node.childNodes.length) {
|
||||
this.pillifyNotificationKeywords(node.childNodes as NodeListOf<Element>, exp);
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
private regExpForKeywordPattern(pattern: string): RegExp {
|
||||
// Reflects the push notification pattern-matching implementation at
|
||||
// https://github.com/matrix-org/matrix-js-sdk/blob/dbd7d26968b94700827bac525c39afff2c198e61/src/pushprocessor.ts#L570
|
||||
return new RegExp("(^|\\W)(" + globToRegexp(pattern) + ")(\\W|$)", "i");
|
||||
}
|
||||
|
||||
private findLinks(nodes: ArrayLike<Element>): string[] {
|
||||
let links: string[] = [];
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ import { asyncSome } from "../../../utils/arrays";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import CopyableText from "../elements/CopyableText";
|
||||
import { useUserTimezone } from "../../../hooks/useUserTimezone";
|
||||
import { useUserProfileValue } from "../../../hooks/useUserProfileValue";
|
||||
export interface IDevice extends Device {
|
||||
ambiguous?: boolean;
|
||||
}
|
||||
@@ -1634,6 +1635,8 @@ export const UserInfoHeader: React.FC<{
|
||||
roomId?: string;
|
||||
}> = ({ member, e2eStatus, roomId }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
console.log(cli, member);
|
||||
const statusMessage = useUserProfileValue(cli, "uk.half-shot.status", member.userId, true);
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
|
||||
@@ -1667,6 +1670,7 @@ export const UserInfoHeader: React.FC<{
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
|
||||
showPresence = enablePresenceByHsUrl[cli.baseUrl];
|
||||
}
|
||||
|
||||
|
||||
let presenceLabel: JSX.Element | undefined;
|
||||
if (showPresence) {
|
||||
@@ -1675,10 +1679,13 @@ export const UserInfoHeader: React.FC<{
|
||||
activeAgo={presenceLastActiveAgo}
|
||||
currentlyActive={presenceCurrentlyActive}
|
||||
presenceState={presenceState}
|
||||
statusMessage={statusMessage ?? undefined}
|
||||
className="mx_UserInfo_profileStatus"
|
||||
coloured
|
||||
/>
|
||||
);
|
||||
} else if (statusMessage) {
|
||||
presenceLabel = <span>{statusMessage}</span>
|
||||
}
|
||||
|
||||
const timezoneInfo = useUserTimezone(cli, member.userId);
|
||||
|
||||
@@ -19,9 +19,10 @@ interface IEmojiButtonProps {
|
||||
addEmoji: (unicode: string) => boolean;
|
||||
menuPosition?: MenuProps;
|
||||
className?: string;
|
||||
single?: boolean;
|
||||
}
|
||||
|
||||
export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonProps): JSX.Element {
|
||||
export function EmojiButton({ addEmoji, menuPosition, className, single }: IEmojiButtonProps): JSX.Element {
|
||||
const overflowMenuCloser = useContext(OverflowMenuContext);
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
@@ -32,10 +33,19 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP
|
||||
closeMenu();
|
||||
overflowMenuCloser?.();
|
||||
};
|
||||
const onChoose = (emoji: string) => {
|
||||
try {
|
||||
return addEmoji(emoji);
|
||||
} finally {
|
||||
if (single) {
|
||||
onFinished();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
contextMenu = (
|
||||
<ContextMenu {...position} onFinished={onFinished} managed={false}>
|
||||
<EmojiPicker onChoose={addEmoji} onFinished={onFinished} />
|
||||
<EmojiPicker onChoose={onChoose} onFinished={onFinished} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
|
||||
/>
|
||||
);
|
||||
const name = vm.name;
|
||||
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
|
||||
const nameJSX = <DisambiguatedProfile statusMessage={vm.statusMessage} member={member} fallbackName={name || ""} />;
|
||||
|
||||
const presenceState = member.presenceState;
|
||||
let presenceJSX: JSX.Element | undefined;
|
||||
|
||||
@@ -27,6 +27,7 @@ interface IProps {
|
||||
// whether to apply colouring to the label
|
||||
coloured?: boolean;
|
||||
className?: string;
|
||||
statusMessage?: string;
|
||||
}
|
||||
|
||||
export default class PresenceLabel extends React.Component<IProps> {
|
||||
@@ -57,7 +58,7 @@ export default class PresenceLabel extends React.Component<IProps> {
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
return <>
|
||||
<div
|
||||
className={classNames("mx_PresenceLabel", this.props.className, {
|
||||
mx_PresenceLabel_online: this.props.coloured && this.props.presenceState === "online",
|
||||
@@ -65,6 +66,8 @@ export default class PresenceLabel extends React.Component<IProps> {
|
||||
>
|
||||
{this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive)}
|
||||
</div>
|
||||
);
|
||||
|
||||
<p>{this.props.statusMessage}</p>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { useUserProfileValue } from "../../../hooks/useUserProfileValue";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { EmojiButton } from "../rooms/EmojiButton";
|
||||
|
||||
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
||||
<>
|
||||
@@ -114,9 +117,21 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
||||
const [maxUploadSize, setMaxUploadSize] = useState<number | undefined>();
|
||||
const [displayNameError, setDisplayNameError] = useState<boolean>(false);
|
||||
|
||||
const [statusMessageSupported, setStatusMessageSupported] = useState<boolean>(false);
|
||||
const [statusMessageError, setStatusMessageError] = useState<boolean>(false);
|
||||
|
||||
const toastRack = useToastContext();
|
||||
|
||||
const client = useMatrixClientContext();
|
||||
const currentStatusMessage = useUserProfileValue(client, "uk.half-shot.status", client.getSafeUserId(), true);
|
||||
const [statusMessage, setStatusMessage] = useState<string>(currentStatusMessage ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStatusMessage) {
|
||||
setStatusMessage(currentStatusMessage);
|
||||
}
|
||||
}, [currentStatusMessage]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -129,6 +144,13 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
||||
})();
|
||||
}, [client]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const supported = await client.doesServerSupportExtendedProfiles();
|
||||
setStatusMessageSupported(supported);
|
||||
})();
|
||||
}, [client]);
|
||||
|
||||
const onAvatarRemove = useCallback(async () => {
|
||||
const removeToast = toastRack.displayToast(
|
||||
<SpinnerToast>{_t("settings|general|avatar_remove_progress")}</SpinnerToast>,
|
||||
@@ -190,6 +212,31 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
||||
[client],
|
||||
);
|
||||
|
||||
const onStatusChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setStatusMessage(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onStatusAddEmoji = useCallback((emoij: string) => {
|
||||
setStatusMessage(s => s + emoij);
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const onStatusCancelled = useCallback(() => {
|
||||
setStatusMessage(currentStatusMessage ?? "");
|
||||
}, [currentStatusMessage]);
|
||||
|
||||
const onStatusSave = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
setStatusMessageError(false);
|
||||
await client.setExtendedProfileProperty("uk.half-shot.status", statusMessage);
|
||||
// Force profile to be refetched.
|
||||
SdkContextClass.instance.userProfilesStore.fetchProfile(client.getSafeUserId());
|
||||
} catch (e) {
|
||||
setStatusMessageError(true);
|
||||
throw e;
|
||||
}
|
||||
}, [statusMessage, client]);
|
||||
|
||||
const someFieldsDisabled = !canSetDisplayName || !canSetAvatar;
|
||||
|
||||
return (
|
||||
@@ -225,6 +272,22 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
||||
>
|
||||
{displayNameError && <ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>}
|
||||
</EditInPlace>
|
||||
<EditInPlace
|
||||
className="mx_UserProfileSettings_profile_statusMessage"
|
||||
label={_t("settings|general|status_message")}
|
||||
value={statusMessage}
|
||||
saveButtonLabel={_t("common|save")}
|
||||
cancelButtonLabel={_t("common|cancel")}
|
||||
savedLabel={_t("common|saved")}
|
||||
savingLabel={_t("common|updating")}
|
||||
onChange={onStatusChanged}
|
||||
onCancel={onStatusCancelled}
|
||||
onSave={onStatusSave}
|
||||
disabled={!statusMessageSupported}
|
||||
>
|
||||
{statusMessageError && <ErrorMessage>{_t("settings|general|status_message_error")}</ErrorMessage>}
|
||||
</EditInPlace>
|
||||
{statusMessageSupported ? <EmojiButton single addEmoji={onStatusAddEmoji}/> : null}
|
||||
</div>
|
||||
{avatarError && (
|
||||
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">
|
||||
|
||||
77
src/hooks/useUserProfileValue.ts
Normal file
77
src/hooks/useUserProfileValue.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { SdkContextClass } from "../contexts/SDKContext";
|
||||
import { UserProfilesStoreEvents } from "../stores/UserProfilesStore";
|
||||
import { useTypedEventEmitter } from "./useEventEmitter";
|
||||
/**
|
||||
* Fetch a key from a user's extended profile, and regularly refetch at a
|
||||
* given interval.
|
||||
*
|
||||
* @param cli The Matrix Client instance.
|
||||
* @param key The key to fetch.
|
||||
* @param userId The user who's profile we're interested in.
|
||||
* @param ignoreCache Ignore the cache on first load.
|
||||
* @returns The value from the profile, or null if not set.
|
||||
*/
|
||||
export const useUserProfileValue = (cli: MatrixClient, key: string, userId?: string, ignoreCache?: boolean): string|null => {
|
||||
const [currentValue, setProfileValue] = useState<string|null>(null);
|
||||
const profilesStore = SdkContextClass.instance.userProfilesStore;
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
profilesStore.subscribeToProfile(userId);
|
||||
return () => profilesStore.unsubscribeToProfile(userId);
|
||||
}, [userId]);
|
||||
|
||||
useTypedEventEmitter(profilesStore, UserProfilesStoreEvents.ProfileUpdated, (updatedUserId, updatedProfile) => {
|
||||
if (userId !== updatedUserId) {
|
||||
return;
|
||||
}
|
||||
const value = (updatedProfile as any)?.[key];
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
// Err, definitely not a tz.
|
||||
throw Error("Profile value was not a string");
|
||||
}
|
||||
setProfileValue(value);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const profile = await (ignoreCache ? profilesStore.fetchProfile(userId) : profilesStore.getOrFetchProfile(userId));
|
||||
// TODO: Types.
|
||||
const value = (profile as any)?.[key];
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
// Err, definitely not a tz.
|
||||
throw Error("Profile value was not a string");
|
||||
}
|
||||
setProfileValue(value);
|
||||
} catch (ex) {
|
||||
if (ex instanceof MatrixError && ex.errcode === "M_NOT_FOUND") {
|
||||
// No timezone set, ignore.
|
||||
setProfileValue(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [userId, key, cli]);
|
||||
|
||||
return currentValue;
|
||||
};
|
||||
@@ -48,7 +48,6 @@ export const useUserTimezone = (cli: MatrixClient, userId: string): { timezone:
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
console.log("Trying to fetch TZ");
|
||||
try {
|
||||
const tz = await cli.getExtendedProfileProperty(userId, "us.cloke.msc4175.tz");
|
||||
if (typeof tz !== "string") {
|
||||
|
||||
@@ -2532,6 +2532,8 @@
|
||||
"remove_email_prompt": "Remove %(email)s?",
|
||||
"remove_msisdn_prompt": "Remove %(phone)s?",
|
||||
"spell_check_locale_placeholder": "Choose a locale",
|
||||
"status_message": "Current status",
|
||||
"status_message_error": "Failed to set status message",
|
||||
"unable_to_load_emails": "Unable to load email addresses",
|
||||
"unable_to_load_msisdns": "Unable to load phone numbers",
|
||||
"username": "Username"
|
||||
@@ -2961,6 +2963,7 @@
|
||||
"error_invalid_room": "Command failed: Unable to find room (%(roomId)s)",
|
||||
"error_invalid_runfn": "Command error: Unable to handle slash command.",
|
||||
"error_invalid_user_in_room": "Could not find user in room",
|
||||
"extended_profile_not_supported": "Status messages are not supported on this server",
|
||||
"help": "Displays list of commands with usages and descriptions",
|
||||
"help_dialog_title": "Command Help",
|
||||
"holdcall": "Places the call in the current room on hold",
|
||||
@@ -2984,6 +2987,7 @@
|
||||
"myroomnick": "Changes your display nickname in the current room only",
|
||||
"nick": "Changes your display nickname",
|
||||
"no_active_call": "No active call in this room",
|
||||
"no_status_message": "You need to provide a status message",
|
||||
"op": "Define the power level of a user",
|
||||
"part_unknown_alias": "Unrecognised room address: %(roomAlias)s",
|
||||
"plain": "Sends a message as plain text, without interpreting it as markdown",
|
||||
@@ -2999,6 +3003,7 @@
|
||||
"server_error_detail": "Server unavailable, overloaded, or something else went wrong.",
|
||||
"shrug": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
|
||||
"spoiler": "Sends the given message as a spoiler",
|
||||
"status_msg": "Update your status message in your profile",
|
||||
"tableflip": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message",
|
||||
"topic": "Gets or sets the room topic",
|
||||
"topic_none": "This room has no topic.",
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
MatrixEvent,
|
||||
RoomMember,
|
||||
RoomMemberEvent,
|
||||
SyncState,
|
||||
TypedEventEmitter,
|
||||
ClientEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { LruCache } from "../utils/LruCache";
|
||||
@@ -27,19 +30,67 @@ interface GetOptions {
|
||||
shouldThrow: boolean;
|
||||
}
|
||||
|
||||
export enum UserProfilesStoreEvents {
|
||||
ProfileUpdated = "profile_updated",
|
||||
}
|
||||
|
||||
interface UserProfilesStoreEventsMap {
|
||||
[UserProfilesStoreEvents.ProfileUpdated]: (userId: string, profile: IMatrixProfile|null) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This store provides cached access to user profiles.
|
||||
* Listens for membership events and invalidates the cache for a profile on update with different profile values.
|
||||
*/
|
||||
export class UserProfilesStore {
|
||||
export class UserProfilesStore extends TypedEventEmitter<UserProfilesStoreEvents, UserProfilesStoreEventsMap> {
|
||||
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||
private profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
|
||||
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||
|
||||
private readonly profileSubscriptions = new Map<string, {subs: number, lastSync: number}>();
|
||||
|
||||
public constructor(private client: MatrixClient) {
|
||||
super();
|
||||
client.on(RoomMemberEvent.Membership, this.onRoomMembershipEvent);
|
||||
client.on(ClientEvent.Sync, (state) => {
|
||||
if (state !== SyncState.Syncing) {
|
||||
return;
|
||||
}
|
||||
const time = Date.now();
|
||||
for (const [userId, entry] of this.profileSubscriptions.entries()) {
|
||||
if (time - entry.lastSync < 60000) {
|
||||
continue;
|
||||
}
|
||||
void this.fetchProfile(userId, { shouldThrow: false});
|
||||
entry.lastSync = time;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public subscribeToProfile(userId: string) {
|
||||
const existingProfile = this.profileSubscriptions.get(userId);
|
||||
console.log(`Sub for ${userId} incremented to ${(existingProfile?.subs ?? 0) + 1}`);
|
||||
if (existingProfile) {
|
||||
existingProfile.subs += 1;
|
||||
} else {
|
||||
this.profileSubscriptions.set(userId, { subs: 1, lastSync: 0})
|
||||
}
|
||||
console.log(`Number of subs: ${this.profileSubscriptions.size}`);
|
||||
}
|
||||
|
||||
public unsubscribeToProfile(userId: string) {
|
||||
const existingProfile = this.profileSubscriptions.get(userId);
|
||||
if (!existingProfile) {
|
||||
return;
|
||||
}
|
||||
existingProfile.subs -= 1;
|
||||
console.log(`Sub for ${userId} reduced to ${existingProfile.subs}`);
|
||||
if (existingProfile.subs === 0) {
|
||||
this.profileSubscriptions.delete(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Synchronously get a profile from the store cache.
|
||||
*
|
||||
@@ -104,6 +155,7 @@ export class UserProfilesStore {
|
||||
public async fetchProfile(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
|
||||
const profile = await this.fetchProfileFromApi(userId, options);
|
||||
this.profiles.set(userId, profile);
|
||||
this.emit(UserProfilesStoreEvents.ProfileUpdated, userId, profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,14 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room, MatrixEvent, MatrixEventEvent, MatrixClient, ClientEvent } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
Room,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
MatrixClient,
|
||||
ClientEvent,
|
||||
RoomStateEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import {
|
||||
ClientWidgetApi,
|
||||
@@ -26,7 +33,6 @@ import {
|
||||
WidgetApiFromWidgetAction,
|
||||
WidgetKind,
|
||||
} from "matrix-widget-api";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { EventEmitter } from "events";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@@ -56,6 +62,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import Modal from "../../Modal";
|
||||
import ErrorDialog from "../../components/views/dialogs/ErrorDialog";
|
||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
|
||||
// TODO: Destroy all of this code
|
||||
|
||||
@@ -151,6 +158,9 @@ export class StopGapWidget extends EventEmitter {
|
||||
private mockWidget: ElementWidget;
|
||||
private scalarToken?: string;
|
||||
private roomId?: string;
|
||||
// The room that we're currently allowing the widget to interact with. Only
|
||||
// used for account widgets, which may follow the user to different rooms.
|
||||
private viewedRoomId: string | null = null;
|
||||
private kind: WidgetKind;
|
||||
private readonly virtual: boolean;
|
||||
private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID
|
||||
@@ -177,17 +187,6 @@ export class StopGapWidget extends EventEmitter {
|
||||
this.stickyPromise = appTileProps.stickyPromise;
|
||||
}
|
||||
|
||||
private get eventListenerRoomId(): Optional<string> {
|
||||
// When widgets are listening to events, we need to make sure they're only
|
||||
// receiving events for the right room. In particular, room widgets get locked
|
||||
// to the room they were added in while account widgets listen to the currently
|
||||
// active room.
|
||||
|
||||
if (this.roomId) return this.roomId;
|
||||
|
||||
return SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
}
|
||||
|
||||
public get widgetApi(): ClientWidgetApi | null {
|
||||
return this.messaging;
|
||||
}
|
||||
@@ -259,6 +258,17 @@ export class StopGapWidget extends EventEmitter {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// This listener is only active for account widgets, which may follow the
|
||||
// user to different rooms
|
||||
private onRoomViewStoreUpdate = (): void => {
|
||||
const roomId = SdkContextClass.instance.roomViewStore.getRoomId() ?? null;
|
||||
if (roomId !== this.viewedRoomId) {
|
||||
this.messaging!.setViewedRoomId(roomId);
|
||||
this.viewedRoomId = roomId;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This starts the messaging for the widget if it is not in the state `started` yet.
|
||||
* @param iframe the iframe the widget should use
|
||||
@@ -285,6 +295,17 @@ export class StopGapWidget extends EventEmitter {
|
||||
this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified"));
|
||||
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
|
||||
|
||||
// When widgets are listening to events, we need to make sure they're only
|
||||
// receiving events for the right room
|
||||
if (this.roomId === undefined) {
|
||||
// Account widgets listen to the currently active room
|
||||
this.messaging.setViewedRoomId(SdkContextClass.instance.roomViewStore.getRoomId() ?? null);
|
||||
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
} else {
|
||||
// Room widgets get locked to the room they were added in
|
||||
this.messaging.setViewedRoomId(this.roomId);
|
||||
}
|
||||
|
||||
// Always attach a handler for ViewRoom, but permission check it internally
|
||||
this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => {
|
||||
ev.preventDefault(); // stop the widget API from auto-rejecting this
|
||||
@@ -329,6 +350,7 @@ export class StopGapWidget extends EventEmitter {
|
||||
// Attach listeners for feeding events - the underlying widget classes handle permissions for us
|
||||
this.client.on(ClientEvent.Event, this.onEvent);
|
||||
this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
this.client.on(RoomStateEvent.Events, this.onStateUpdate);
|
||||
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
|
||||
this.messaging.on(
|
||||
@@ -457,8 +479,11 @@ export class StopGapWidget extends EventEmitter {
|
||||
WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId);
|
||||
this.messaging = null;
|
||||
|
||||
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
|
||||
this.client.off(ClientEvent.Event, this.onEvent);
|
||||
this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
this.client.off(RoomStateEvent.Events, this.onStateUpdate);
|
||||
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
}
|
||||
|
||||
@@ -471,6 +496,14 @@ export class StopGapWidget extends EventEmitter {
|
||||
this.feedEvent(ev);
|
||||
};
|
||||
|
||||
private onStateUpdate = (ev: MatrixEvent): void => {
|
||||
if (this.messaging === null) return;
|
||||
const raw = ev.getEffectiveEvent();
|
||||
this.messaging.feedStateUpdate(raw as IRoomEvent).catch((e) => {
|
||||
logger.error("Error sending state update to widget: ", e);
|
||||
});
|
||||
};
|
||||
|
||||
private onToDeviceEvent = async (ev: MatrixEvent): Promise<void> => {
|
||||
await this.client.decryptEventIfNeeded(ev);
|
||||
if (ev.isDecryptionFailure()) return;
|
||||
@@ -570,7 +603,7 @@ export class StopGapWidget extends EventEmitter {
|
||||
this.eventsToFeed.add(ev);
|
||||
} else {
|
||||
const raw = ev.getEffectiveEvent();
|
||||
this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => {
|
||||
this.messaging.feedEvent(raw as IRoomEvent).catch((e) => {
|
||||
logger.error("Error sending event to widget: ", e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
MatrixCapabilities,
|
||||
OpenIDRequestState,
|
||||
SimpleObservable,
|
||||
Symbols,
|
||||
Widget,
|
||||
WidgetDriver,
|
||||
WidgetEventCapability,
|
||||
@@ -36,7 +35,6 @@ import {
|
||||
IContent,
|
||||
MatrixError,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
Direction,
|
||||
THREAD_RELATION_TYPE,
|
||||
SendDelayedEventResponse,
|
||||
@@ -469,70 +467,69 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||
}
|
||||
}
|
||||
|
||||
private pickRooms(roomIds?: (string | Symbols.AnyRoom)[]): Room[] {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) throw new Error("Not attached to a client");
|
||||
|
||||
const targetRooms = roomIds
|
||||
? roomIds.includes(Symbols.AnyRoom)
|
||||
? client.getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors"))
|
||||
: roomIds.map((r) => client.getRoom(r))
|
||||
: [client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()!)];
|
||||
return targetRooms.filter((r) => !!r) as Room[];
|
||||
}
|
||||
|
||||
public async readRoomEvents(
|
||||
/**
|
||||
* Reads all events of the given type, and optionally `msgtype` (if applicable/defined),
|
||||
* the user has access to. The widget API will have already verified that the widget is
|
||||
* capable of receiving the events. Less events than the limit are allowed to be returned,
|
||||
* but not more.
|
||||
* @param roomId The ID of the room to look within.
|
||||
* @param eventType The event type to be read.
|
||||
* @param msgtype The msgtype of the events to be read, if applicable/defined.
|
||||
* @param stateKey The state key of the events to be read, if applicable/defined.
|
||||
* @param limit The maximum number of events to retrieve. Will be zero to denote "as many as
|
||||
* possible".
|
||||
* @param since When null, retrieves the number of events specified by the "limit" parameter.
|
||||
* Otherwise, the event ID at which only subsequent events will be returned, as many as specified
|
||||
* in "limit".
|
||||
* @returns {Promise<IRoomEvent[]>} Resolves to the room events, or an empty array.
|
||||
*/
|
||||
public async readRoomTimeline(
|
||||
roomId: string,
|
||||
eventType: string,
|
||||
msgtype: string | undefined,
|
||||
limitPerRoom: number,
|
||||
roomIds?: (string | Symbols.AnyRoom)[],
|
||||
stateKey: string | undefined,
|
||||
limit: number,
|
||||
since: string | undefined,
|
||||
): Promise<IRoomEvent[]> {
|
||||
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
|
||||
limit = limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
|
||||
|
||||
const rooms = this.pickRooms(roomIds);
|
||||
const allResults: IRoomEvent[] = [];
|
||||
for (const room of rooms) {
|
||||
const results: MatrixEvent[] = [];
|
||||
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
|
||||
for (let i = events.length - 1; i > 0; i--) {
|
||||
if (results.length >= limitPerRoom) break;
|
||||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (room === null) return [];
|
||||
const results: MatrixEvent[] = [];
|
||||
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
const ev = events[i];
|
||||
if (results.length >= limit) break;
|
||||
if (since !== undefined && ev.getId() === since) break;
|
||||
|
||||
const ev = events[i];
|
||||
if (ev.getType() !== eventType || ev.isState()) continue;
|
||||
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue;
|
||||
results.push(ev);
|
||||
}
|
||||
|
||||
results.forEach((e) => allResults.push(e.getEffectiveEvent() as IRoomEvent));
|
||||
if (ev.getType() !== eventType || ev.isState()) continue;
|
||||
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue;
|
||||
if (ev.getStateKey() !== undefined && stateKey !== undefined && ev.getStateKey() !== stateKey) continue;
|
||||
results.push(ev);
|
||||
}
|
||||
return allResults;
|
||||
|
||||
return results.map((e) => e.getEffectiveEvent() as IRoomEvent);
|
||||
}
|
||||
|
||||
public async readStateEvents(
|
||||
eventType: string,
|
||||
stateKey: string | undefined,
|
||||
limitPerRoom: number,
|
||||
roomIds?: (string | Symbols.AnyRoom)[],
|
||||
): Promise<IRoomEvent[]> {
|
||||
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
|
||||
/**
|
||||
* Reads the current values of all matching room state entries.
|
||||
* @param roomId The ID of the room.
|
||||
* @param eventType The event type of the entries to be read.
|
||||
* @param stateKey The state key of the entry to be read. If undefined,
|
||||
* all room state entries with a matching event type should be returned.
|
||||
* @returns {Promise<IRoomEvent[]>} Resolves to the events representing the
|
||||
* current values of the room state entries.
|
||||
*/
|
||||
public async readRoomState(roomId: string, eventType: string, stateKey: string | undefined): Promise<IRoomEvent[]> {
|
||||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (room === null) return [];
|
||||
const state = room.getLiveTimeline().getState(Direction.Forward);
|
||||
if (state === undefined) return [];
|
||||
|
||||
const rooms = this.pickRooms(roomIds);
|
||||
const allResults: IRoomEvent[] = [];
|
||||
for (const room of rooms) {
|
||||
const results: MatrixEvent[] = [];
|
||||
const state = room.currentState.events.get(eventType);
|
||||
if (state) {
|
||||
if (stateKey === "" || !!stateKey) {
|
||||
const forKey = state.get(stateKey);
|
||||
if (forKey) results.push(forKey);
|
||||
} else {
|
||||
results.push(...Array.from(state.values()));
|
||||
}
|
||||
}
|
||||
|
||||
results.slice(0, limitPerRoom).forEach((e) => allResults.push(e.getEffectiveEvent() as IRoomEvent));
|
||||
}
|
||||
return allResults;
|
||||
if (stateKey === undefined)
|
||||
return state.getStateEvents(eventType).map((e) => e.getEffectiveEvent() as IRoomEvent);
|
||||
const event = state.getStateEvents(eventType, stateKey);
|
||||
return event === null ? [] : [event.getEffectiveEvent() as IRoomEvent];
|
||||
}
|
||||
|
||||
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>): Promise<void> {
|
||||
@@ -693,6 +690,17 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||
return { file: blob };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IDs of all joined or invited rooms currently known to the
|
||||
* client.
|
||||
* @returns The room IDs.
|
||||
*/
|
||||
public getKnownRooms(): string[] {
|
||||
return MatrixClientPeg.safeGet()
|
||||
.getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors"))
|
||||
.map((r) => r.roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expresses a {@link MatrixError} as a JSON payload
|
||||
* for use by Widget API error responses.
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient, MatrixEvent, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
|
||||
@@ -228,6 +228,23 @@ describe("<TextualBody />", () => {
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should pillify a keyword responsible for triggering a notification", () => {
|
||||
const ev = mkRoomTextMessage("foo bar baz");
|
||||
ev.setPushDetails(undefined, {
|
||||
actions: [],
|
||||
pattern: "bar",
|
||||
rule_id: "bar",
|
||||
default: false,
|
||||
enabled: true,
|
||||
kind: PushRuleKind.ContentSpecific,
|
||||
});
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML).toMatchInlineSnapshot(
|
||||
`"<span>foo <bdi><span tabindex="0"><span class="mx_Pill mx_KeywordPill"><span class="mx_Pill_text">bar</span></span></span></bdi> baz</span>"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renders formatted m.text correctly", () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { mocked, MockedFunction, MockedObject } from "jest-mock";
|
||||
import { last } from "lodash";
|
||||
import {
|
||||
MatrixEvent,
|
||||
@@ -15,15 +15,20 @@ import {
|
||||
EventTimeline,
|
||||
EventType,
|
||||
MatrixEventEvent,
|
||||
RoomStateEvent,
|
||||
RoomState,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import { stubClient, mkRoom, mkEvent } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget";
|
||||
import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||
import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore";
|
||||
|
||||
jest.mock("matrix-widget-api/lib/ClientWidgetApi");
|
||||
|
||||
@@ -53,6 +58,7 @@ describe("StopGapWidget", () => {
|
||||
// Start messaging without an iframe, since ClientWidgetApi is mocked
|
||||
widget.startMessaging(null as unknown as HTMLIFrameElement);
|
||||
messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!);
|
||||
messaging.feedStateUpdate.mockResolvedValue();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -84,6 +90,20 @@ describe("StopGapWidget", () => {
|
||||
expect(messaging.feedToDevice).toHaveBeenCalledWith(event.getEffectiveEvent(), false);
|
||||
});
|
||||
|
||||
it("feeds incoming state updates to the widget", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
type: "org.example.foo",
|
||||
skey: "",
|
||||
user: "@alice:example.org",
|
||||
content: { hello: "world" },
|
||||
room: "!1:example.org",
|
||||
});
|
||||
|
||||
client.emit(RoomStateEvent.Events, event, {} as unknown as RoomState, null);
|
||||
expect(messaging.feedStateUpdate).toHaveBeenCalledWith(event.getEffectiveEvent());
|
||||
});
|
||||
|
||||
describe("feed event", () => {
|
||||
let event1: MatrixEvent;
|
||||
let event2: MatrixEvent;
|
||||
@@ -118,24 +138,24 @@ describe("StopGapWidget", () => {
|
||||
|
||||
it("feeds incoming event to the widget", async () => {
|
||||
client.emit(ClientEvent.Event, event1);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
|
||||
|
||||
client.emit(ClientEvent.Event, event2);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
|
||||
});
|
||||
|
||||
it("should not feed incoming event to the widget if seen already", async () => {
|
||||
client.emit(ClientEvent.Event, event1);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
|
||||
|
||||
client.emit(ClientEvent.Event, event2);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
|
||||
|
||||
client.emit(ClientEvent.Event, event1);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
|
||||
});
|
||||
|
||||
it("feeds decrypted events asynchronously", async () => {
|
||||
@@ -165,7 +185,7 @@ describe("StopGapWidget", () => {
|
||||
decryptingSpy2.mockReturnValue(false);
|
||||
client.emit(MatrixEventEvent.Decrypted, event2Encrypted);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(1);
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent());
|
||||
// …then event 1
|
||||
event1Encrypted.event.type = event1.getType();
|
||||
event1Encrypted.event.content = event1.getContent();
|
||||
@@ -175,7 +195,7 @@ describe("StopGapWidget", () => {
|
||||
// doesn't have to be blocked on the decryption of event 1 (or
|
||||
// worse, dropped)
|
||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent());
|
||||
});
|
||||
|
||||
it("should not feed incoming event if not in timeline", () => {
|
||||
@@ -191,7 +211,7 @@ describe("StopGapWidget", () => {
|
||||
});
|
||||
|
||||
client.emit(ClientEvent.Event, event);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event.getEffectiveEvent());
|
||||
});
|
||||
|
||||
it("feeds incoming event that is not in timeline but relates to unknown parent to the widget", async () => {
|
||||
@@ -211,18 +231,19 @@ describe("StopGapWidget", () => {
|
||||
});
|
||||
|
||||
client.emit(ClientEvent.Event, event1);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
|
||||
|
||||
client.emit(ClientEvent.Event, event);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent());
|
||||
|
||||
client.emit(ClientEvent.Event, event1);
|
||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org");
|
||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("StopGapWidget with stickyPromise", () => {
|
||||
let client: MockedObject<MatrixClient>;
|
||||
let widget: StopGapWidget;
|
||||
@@ -288,3 +309,49 @@ describe("StopGapWidget with stickyPromise", () => {
|
||||
waitFor(() => expect(setPersistenceSpy).toHaveBeenCalled(), { interval: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("StopGapWidget as an account widget", () => {
|
||||
let widget: StopGapWidget;
|
||||
let messaging: MockedObject<ClientWidgetApi>;
|
||||
let getRoomId: MockedFunction<() => Optional<string>>;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
// I give up, getting the return type of spyOn right is hopeless
|
||||
getRoomId = jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId") as unknown as MockedFunction<
|
||||
() => Optional<string>
|
||||
>;
|
||||
getRoomId.mockReturnValue("!1:example.org");
|
||||
|
||||
widget = new StopGapWidget({
|
||||
app: {
|
||||
id: "test",
|
||||
creatorUserId: "@alice:example.org",
|
||||
type: "example",
|
||||
url: "https://example.org?user-id=$matrix_user_id&device-id=$org.matrix.msc3819.matrix_device_id&base-url=$org.matrix.msc4039.matrix_base_url&theme=$org.matrix.msc2873.client_theme",
|
||||
roomId: "!1:example.org",
|
||||
},
|
||||
userId: "@alice:example.org",
|
||||
creatorUserId: "@alice:example.org",
|
||||
waitForIframeLoad: true,
|
||||
userWidget: false,
|
||||
});
|
||||
// Start messaging without an iframe, since ClientWidgetApi is mocked
|
||||
widget.startMessaging(null as unknown as HTMLIFrameElement);
|
||||
messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
widget.stopMessaging();
|
||||
getRoomId.mockRestore();
|
||||
});
|
||||
|
||||
it("updates viewed room", () => {
|
||||
expect(messaging.setViewedRoomId).toHaveBeenCalledTimes(1);
|
||||
expect(messaging.setViewedRoomId).toHaveBeenLastCalledWith("!1:example.org");
|
||||
getRoomId.mockReturnValue("!2:example.org");
|
||||
SdkContextClass.instance.roomViewStore.emit(UPDATE_EVENT);
|
||||
expect(messaging.setViewedRoomId).toHaveBeenCalledTimes(2);
|
||||
expect(messaging.setViewedRoomId).toHaveBeenLastCalledWith("!2:example.org");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
MatrixEvent,
|
||||
MsgType,
|
||||
RelationType,
|
||||
Room,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
Widget,
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { mkEvent, stubClient } from "../../../test-utils";
|
||||
import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
|
||||
import dis from "../../../../src/dispatcher/dispatcher";
|
||||
import Modal from "../../../../src/Modal";
|
||||
@@ -569,7 +570,7 @@ describe("StopGapWidgetDriver", () => {
|
||||
|
||||
it("passes the flag through to getVisibleRooms", () => {
|
||||
const driver = mkDefaultDriver();
|
||||
driver.readRoomEvents(EventType.CallAnswer, "", 0, ["*"]);
|
||||
driver.getKnownRooms();
|
||||
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
@@ -584,7 +585,7 @@ describe("StopGapWidgetDriver", () => {
|
||||
|
||||
it("passes the flag through to getVisibleRooms", () => {
|
||||
const driver = mkDefaultDriver();
|
||||
driver.readRoomEvents(EventType.CallAnswer, "", 0, ["*"]);
|
||||
driver.getKnownRooms();
|
||||
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -692,4 +693,107 @@ describe("StopGapWidgetDriver", () => {
|
||||
await expect(file.text()).resolves.toEqual("test contents");
|
||||
});
|
||||
});
|
||||
|
||||
describe("readRoomTimeline", () => {
|
||||
const event1 = mkEvent({
|
||||
event: true,
|
||||
id: "$event-id1",
|
||||
type: "org.example.foo",
|
||||
user: "@alice:example.org",
|
||||
content: { hello: "world" },
|
||||
room: "!1:example.org",
|
||||
});
|
||||
const event2 = mkEvent({
|
||||
event: true,
|
||||
id: "$event-id2",
|
||||
type: "org.example.foo",
|
||||
user: "@alice:example.org",
|
||||
content: { hello: "world" },
|
||||
room: "!1:example.org",
|
||||
});
|
||||
let driver: WidgetDriver;
|
||||
|
||||
beforeEach(() => {
|
||||
driver = mkDefaultDriver();
|
||||
client.getRoom.mockReturnValue({
|
||||
getLiveTimeline: () => ({ getEvents: () => [event1, event2] }),
|
||||
} as unknown as Room);
|
||||
});
|
||||
|
||||
it("reads all events", async () => {
|
||||
expect(
|
||||
await driver.readRoomTimeline("!1:example.org", "org.example.foo", undefined, undefined, 10, undefined),
|
||||
).toEqual([event2, event1].map((e) => e.getEffectiveEvent()));
|
||||
});
|
||||
|
||||
it("reads up to a limit", async () => {
|
||||
expect(
|
||||
await driver.readRoomTimeline("!1:example.org", "org.example.foo", undefined, undefined, 1, undefined),
|
||||
).toEqual([event2.getEffectiveEvent()]);
|
||||
});
|
||||
|
||||
it("reads up to a specific event", async () => {
|
||||
expect(
|
||||
await driver.readRoomTimeline(
|
||||
"!1:example.org",
|
||||
"org.example.foo",
|
||||
undefined,
|
||||
undefined,
|
||||
10,
|
||||
event1.getId(),
|
||||
),
|
||||
).toEqual([event2.getEffectiveEvent()]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readRoomState", () => {
|
||||
const event1 = mkEvent({
|
||||
event: true,
|
||||
id: "$event-id1",
|
||||
type: "org.example.foo",
|
||||
user: "@alice:example.org",
|
||||
content: { hello: "world" },
|
||||
skey: "1",
|
||||
room: "!1:example.org",
|
||||
});
|
||||
const event2 = mkEvent({
|
||||
event: true,
|
||||
id: "$event-id2",
|
||||
type: "org.example.foo",
|
||||
user: "@alice:example.org",
|
||||
content: { hello: "world" },
|
||||
skey: "2",
|
||||
room: "!1:example.org",
|
||||
});
|
||||
let driver: WidgetDriver;
|
||||
let getStateEvents: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
driver = mkDefaultDriver();
|
||||
getStateEvents = jest.fn();
|
||||
client.getRoom.mockReturnValue({
|
||||
getLiveTimeline: () => ({ getState: () => ({ getStateEvents }) }),
|
||||
} as unknown as Room);
|
||||
});
|
||||
|
||||
it("reads a specific state key", async () => {
|
||||
getStateEvents.mockImplementation((eventType, stateKey) => {
|
||||
if (eventType === "org.example.foo" && stateKey === "1") return event1;
|
||||
return undefined;
|
||||
});
|
||||
expect(await driver.readRoomState("!1:example.org", "org.example.foo", "1")).toEqual([
|
||||
event1.getEffectiveEvent(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads all state keys", async () => {
|
||||
getStateEvents.mockImplementation((eventType, stateKey) => {
|
||||
if (eventType === "org.example.foo" && stateKey === undefined) return [event1, event2];
|
||||
return [];
|
||||
});
|
||||
expect(await driver.readRoomState("!1:example.org", "org.example.foo", undefined)).toEqual(
|
||||
[event1, event2].map((e) => e.getEffectiveEvent()),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
111
yarn.lock
111
yarn.lock
@@ -2731,11 +2731,11 @@
|
||||
"@svgr/plugin-svgo" "8.1.0"
|
||||
|
||||
"@testcontainers/postgresql@^10.16.0":
|
||||
version "10.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-10.16.0.tgz#0437a9b426d64ea958e745a0e2ae19462b786f81"
|
||||
integrity sha512-zWFQI+3QxlEELRvVv27i6zlVEPNUz9zKaSh7iWmFlCdfhcyr78daS0FG8FIfdQ79VK7YXA4jv+dTYXa2SwXu/w==
|
||||
version "10.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-10.17.1.tgz#a0482b3eef094bcda885c3b96eeae7c737fa58bc"
|
||||
integrity sha512-TXFU7ptv8pTVscA7sd5NtSuxO3IP/d8I/plOliVyH+w8IRb1LdXxe/BEgJrwhKgVefarYvgHeBPgqjG3Jef+3A==
|
||||
dependencies:
|
||||
testcontainers "^10.16.0"
|
||||
testcontainers "^10.17.1"
|
||||
|
||||
"@testing-library/dom@^10.4.0":
|
||||
version "10.4.0"
|
||||
@@ -2903,9 +2903,9 @@
|
||||
"@types/ssh2" "*"
|
||||
|
||||
"@types/dockerode@^3.3.29":
|
||||
version "3.3.33"
|
||||
resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.33.tgz#67d9b4223caf41a0735695abe89c292e05d305c9"
|
||||
integrity sha512-7av8lVOhkW7Xd11aZTSq5zhdpyNraldXwQR0pxUCiSNTvIzsP86KrFrmrZgxtrXD2Zrtzwt4H6OYLbATONWzWg==
|
||||
version "3.3.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.34.tgz#1cef62f1b98f80bd4460961dd8aac99b95a0fb6e"
|
||||
integrity sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg==
|
||||
dependencies:
|
||||
"@types/docker-modem" "*"
|
||||
"@types/node" "*"
|
||||
@@ -3153,20 +3153,13 @@
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
"@types/node@18":
|
||||
"@types/node@18", "@types/node@^18.11.18":
|
||||
version "18.19.71"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.71.tgz#96d4f0a0be735ead6c8998c62a4b2c0012a5d09a"
|
||||
integrity sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@^18.11.18":
|
||||
version "18.19.69"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.69.tgz#748d301818ba4b238854c53d290257a70aae7d01"
|
||||
integrity sha512-ECPdY1nlaiO/Y6GUnwgtAAhLNaQ53AyIVz+eILxpEo5OvuqE6yWkqWBIb5dU0DqhKQtMeny+FBD3PK6lm7L5xQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
|
||||
@@ -3318,9 +3311,9 @@
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ssh2@*":
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.15.1.tgz#4db4b6864abca09eb299fe5354fa591add412223"
|
||||
integrity sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==
|
||||
version "1.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.15.4.tgz#2347d2ff079e205b077c02407d822803bfd23c45"
|
||||
integrity sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA==
|
||||
dependencies:
|
||||
"@types/node" "^18.11.18"
|
||||
|
||||
@@ -4204,35 +4197,35 @@ balanced-match@^2.0.0:
|
||||
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
|
||||
|
||||
bare-events@^2.0.0, bare-events@^2.2.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc"
|
||||
integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==
|
||||
version "2.5.4"
|
||||
resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.4.tgz#16143d435e1ed9eafd1ab85f12b89b3357a41745"
|
||||
integrity sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==
|
||||
|
||||
bare-fs@^2.1.1:
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-2.3.5.tgz#05daa8e8206aeb46d13c2fe25a2cd3797b0d284a"
|
||||
integrity sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==
|
||||
bare-fs@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-4.0.1.tgz#85844f34da819c76754d545323a8b23ed3617c76"
|
||||
integrity sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==
|
||||
dependencies:
|
||||
bare-events "^2.0.0"
|
||||
bare-path "^2.0.0"
|
||||
bare-path "^3.0.0"
|
||||
bare-stream "^2.0.0"
|
||||
|
||||
bare-os@^2.1.0:
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-2.4.4.tgz#01243392eb0a6e947177bb7c8a45123d45c9b1a9"
|
||||
integrity sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==
|
||||
bare-os@^3.0.1:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-3.4.0.tgz#97be31503f3095beb232a6871f0118859832eb0c"
|
||||
integrity sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==
|
||||
|
||||
bare-path@^2.0.0, bare-path@^2.1.0:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-2.1.3.tgz#594104c829ef660e43b5589ec8daef7df6cedb3e"
|
||||
integrity sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==
|
||||
bare-path@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-3.0.0.tgz#b59d18130ba52a6af9276db3e96a2e3d3ea52178"
|
||||
integrity sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==
|
||||
dependencies:
|
||||
bare-os "^2.1.0"
|
||||
bare-os "^3.0.1"
|
||||
|
||||
bare-stream@^2.0.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.6.1.tgz#b3b9874fab05b662c9aea2706a12fb0698c46836"
|
||||
integrity sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g==
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.6.4.tgz#4226bc8ec7b3ff2c17087385326909978747b149"
|
||||
integrity sha512-G6i3A74FjNq4nVrrSTUz5h3vgXzBJnjmWAVlBWaZETkgu+LgKd7AiyOml3EDJY1AHlIbBHKDXE+TUT53Ff8OaA==
|
||||
dependencies:
|
||||
streamx "^2.21.0"
|
||||
|
||||
@@ -5250,7 +5243,7 @@ debug@2.6.9:
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7, debug@~4.4.0:
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7, debug@~4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
|
||||
@@ -5264,7 +5257,7 @@ debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.1.1, debug@^4.3.2:
|
||||
debug@^4.3.2:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
|
||||
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
|
||||
@@ -8693,10 +8686,10 @@ matrix-web-i18n@^3.2.1:
|
||||
minimist "^1.2.8"
|
||||
walk "^2.3.15"
|
||||
|
||||
matrix-widget-api@1.11.0, matrix-widget-api@^1.10.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.11.0.tgz#2f548b11a7c0df789d5d4fdb5cc9ef7af8aef3da"
|
||||
integrity sha512-ED/9hrJqDWVLeED0g1uJnYRhINh3ZTquwurdM+Hc8wLVJIQ8G/r7A7z74NC+8bBIHQ1Jo7i1Uq5CoJp/TzFYrA==
|
||||
matrix-widget-api@^1.10.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.12.0.tgz#b3d22bab1670051c8eeee66bb96d08b33148bc99"
|
||||
integrity sha512-6JRd9fJGGvuBRhcTg9wX+Skn/Q1wox3jdp5yYQKJ6pPw4urW9bkTR90APBKVDB1vorJKT44jml+lCzkDMRBjww==
|
||||
dependencies:
|
||||
"@types/events" "^3.0.0"
|
||||
events "^3.2.0"
|
||||
@@ -10579,9 +10572,9 @@ readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@^4.0.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.6.0.tgz#ce412dfb19c04efde1c5936d99c27f37a1ff94c9"
|
||||
integrity sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw==
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91"
|
||||
integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==
|
||||
dependencies:
|
||||
abort-controller "^3.0.0"
|
||||
buffer "^6.0.3"
|
||||
@@ -11780,15 +11773,15 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
|
||||
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
|
||||
|
||||
tar-fs@^3.0.6:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.6.tgz#eaccd3a67d5672f09ca8e8f9c3d2b89fa173f217"
|
||||
integrity sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.8.tgz#8f62012537d5ff89252d01e48690dc4ebed33ab7"
|
||||
integrity sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==
|
||||
dependencies:
|
||||
pump "^3.0.0"
|
||||
tar-stream "^3.1.5"
|
||||
optionalDependencies:
|
||||
bare-fs "^2.1.1"
|
||||
bare-path "^2.1.0"
|
||||
bare-fs "^4.0.1"
|
||||
bare-path "^3.0.0"
|
||||
|
||||
tar-fs@~2.0.1:
|
||||
version "2.0.1"
|
||||
@@ -11877,10 +11870,10 @@ test-exclude@^6.0.0:
|
||||
glob "^7.1.4"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
testcontainers@^10.16.0:
|
||||
version "10.16.0"
|
||||
resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.16.0.tgz#8a7e69ada5cd2c6cce1c6db72b3a3e8e412fcaf6"
|
||||
integrity sha512-oxPLuOtrRWS11A+Yn0+zXB7GkmNarflWqmy6CQJk8KJ75LZs2/zlUXDpizTbPpCGtk4kE2EQYwFZjrE967F8Wg==
|
||||
testcontainers@^10.16.0, testcontainers@^10.17.1:
|
||||
version "10.17.1"
|
||||
resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.17.1.tgz#075ff24cec1fb550dc9990e33cd8c24e1cb67b82"
|
||||
integrity sha512-pYwpm6iH1UtZFVoSWjfUol4JCMyX4UksA5fwDotlTp2GgMqoHud+A+PY60kYUBVdSJJ/5AsSqhhFRvoK4ijISg==
|
||||
dependencies:
|
||||
"@balena/dockerignore" "^1.0.2"
|
||||
"@types/dockerode" "^3.3.29"
|
||||
@@ -12196,9 +12189,9 @@ undici-types@~6.20.0:
|
||||
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
|
||||
|
||||
undici@^5.28.4:
|
||||
version "5.28.4"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"
|
||||
integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==
|
||||
version "5.28.5"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.5.tgz#b2b94b6bf8f1d919bc5a6f31f2c01deb02e54d4b"
|
||||
integrity sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==
|
||||
dependencies:
|
||||
"@fastify/busboy" "^2.0.0"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user