Merge pull request #9661 from matrix-org/feat/emoji-picker-rich-text-mode
Add emoji handling for rich text mode
This commit is contained in:
commit
982c83d2a8
13 changed files with 249 additions and 16 deletions
|
@ -57,7 +57,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.3.0",
|
"@matrix-org/analytics-events": "^0.3.0",
|
||||||
"@matrix-org/matrix-wysiwyg": "^0.8.0",
|
"@matrix-org/matrix-wysiwyg": "^0.9.0",
|
||||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||||
"@sentry/browser": "^7.0.0",
|
"@sentry/browser": "^7.0.0",
|
||||||
"@sentry/tracing": "^7.0.0",
|
"@sentry/tracing": "^7.0.0",
|
||||||
|
|
|
@ -22,8 +22,8 @@ import { PlainTextComposer } from './components/PlainTextComposer';
|
||||||
import { ComposerFunctions } from './types';
|
import { ComposerFunctions } from './types';
|
||||||
import { E2EStatus } from '../../../../utils/ShieldUtils';
|
import { E2EStatus } from '../../../../utils/ShieldUtils';
|
||||||
import E2EIcon from '../E2EIcon';
|
import E2EIcon from '../E2EIcon';
|
||||||
import { EmojiButton } from '../EmojiButton';
|
|
||||||
import { AboveLeftOf } from '../../../structures/ContextMenu';
|
import { AboveLeftOf } from '../../../structures/ContextMenu';
|
||||||
|
import { Emoji } from './components/Emoji';
|
||||||
|
|
||||||
interface ContentProps {
|
interface ContentProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -58,8 +58,8 @@ export function SendWysiwygComposer(
|
||||||
return <Composer
|
return <Composer
|
||||||
className="mx_SendWysiwygComposer"
|
className="mx_SendWysiwygComposer"
|
||||||
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}
|
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}
|
||||||
// TODO add emoji support
|
rightComponent={(selectPreviousSelection) =>
|
||||||
rightComponent={<EmojiButton menuPosition={menuPosition} addEmoji={() => false} />}
|
<Emoji menuPosition={menuPosition} selectPreviousSelection={selectPreviousSelection} />}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{ (ref, composerFunctions) => (
|
{ (ref, composerFunctions) => (
|
||||||
|
|
|
@ -18,6 +18,7 @@ import classNames from 'classnames';
|
||||||
import React, { CSSProperties, forwardRef, memo, MutableRefObject, ReactNode } from 'react';
|
import React, { CSSProperties, forwardRef, memo, MutableRefObject, ReactNode } from 'react';
|
||||||
|
|
||||||
import { useIsExpanded } from '../hooks/useIsExpanded';
|
import { useIsExpanded } from '../hooks/useIsExpanded';
|
||||||
|
import { useSelection } from '../hooks/useSelection';
|
||||||
|
|
||||||
const HEIGHT_BREAKING_POINT = 20;
|
const HEIGHT_BREAKING_POINT = 20;
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ interface EditorProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
leftComponent?: ReactNode;
|
leftComponent?: ReactNode;
|
||||||
rightComponent?: ReactNode;
|
rightComponent?: (selectPreviousSelection: () => void) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = memo(
|
export const Editor = memo(
|
||||||
|
@ -33,6 +34,7 @@ export const Editor = memo(
|
||||||
function Editor({ disabled, placeholder, leftComponent, rightComponent }: EditorProps, ref,
|
function Editor({ disabled, placeholder, leftComponent, rightComponent }: EditorProps, ref,
|
||||||
) {
|
) {
|
||||||
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
|
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
|
||||||
|
const { onFocus, onBlur, selectPreviousSelection } = useSelection();
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
data-testid="WysiwygComposerEditor"
|
data-testid="WysiwygComposerEditor"
|
||||||
|
@ -55,9 +57,11 @@ export const Editor = memo(
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{ rightComponent }
|
{ rightComponent?.(selectPreviousSelection) }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
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 { AboveLeftOf } from "../../../../structures/ContextMenu";
|
||||||
|
import { EmojiButton } from "../../EmojiButton";
|
||||||
|
import dis from '../../../../../dispatcher/dispatcher';
|
||||||
|
import { ComposerInsertPayload } from "../../../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
|
|
||||||
|
interface EmojiProps {
|
||||||
|
selectPreviousSelection: () => void;
|
||||||
|
menuPosition: AboveLeftOf;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Emoji({ selectPreviousSelection, menuPosition }: EmojiProps) {
|
||||||
|
const roomContext = useRoomContext();
|
||||||
|
|
||||||
|
return <EmojiButton menuPosition={menuPosition}
|
||||||
|
addEmoji={(emoji) => {
|
||||||
|
selectPreviousSelection();
|
||||||
|
dis.dispatch<ComposerInsertPayload>({
|
||||||
|
action: Action.ComposerInsert,
|
||||||
|
text: emoji,
|
||||||
|
timelineRenderingType: roomContext.timelineRenderingType,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}
|
|
@ -33,7 +33,9 @@ interface PlainTextComposerProps {
|
||||||
initialContent?: string;
|
initialContent?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
leftComponent?: ReactNode;
|
leftComponent?: ReactNode;
|
||||||
rightComponent?: ReactNode;
|
rightComponent?: (
|
||||||
|
selectPreviousSelection: () => void
|
||||||
|
) => ReactNode;
|
||||||
children?: (
|
children?: (
|
||||||
ref: MutableRefObject<HTMLDivElement | null>,
|
ref: MutableRefObject<HTMLDivElement | null>,
|
||||||
composerFunctions: ComposerFunctions,
|
composerFunctions: ComposerFunctions,
|
||||||
|
|
|
@ -32,7 +32,9 @@ interface WysiwygComposerProps {
|
||||||
initialContent?: string;
|
initialContent?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
leftComponent?: ReactNode;
|
leftComponent?: ReactNode;
|
||||||
rightComponent?: ReactNode;
|
rightComponent?: (
|
||||||
|
selectPreviousSelection: () => void
|
||||||
|
) => ReactNode;
|
||||||
children?: (
|
children?: (
|
||||||
ref: MutableRefObject<HTMLDivElement | null>,
|
ref: MutableRefObject<HTMLDivElement | null>,
|
||||||
wysiwyg: FormattingFunctions,
|
wysiwyg: FormattingFunctions,
|
||||||
|
|
|
@ -23,5 +23,8 @@ export function useComposerFunctions(ref: RefObject<HTMLDivElement>) {
|
||||||
ref.current.innerHTML = '';
|
ref.current.innerHTML = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
insertText: (text: string) => {
|
||||||
|
// TODO
|
||||||
|
},
|
||||||
}), [ref]);
|
}), [ref]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
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 { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import useFocus from "../../../../../hooks/useFocus";
|
||||||
|
import { setSelection } from "../utils/selection";
|
||||||
|
|
||||||
|
type SubSelection = Pick<Selection, 'anchorNode' | 'anchorOffset' | 'focusNode' | 'focusOffset'>;
|
||||||
|
|
||||||
|
export function useSelection() {
|
||||||
|
const selectionRef = useRef<SubSelection>({
|
||||||
|
anchorNode: null,
|
||||||
|
anchorOffset: 0,
|
||||||
|
focusNode: null,
|
||||||
|
focusOffset: 0,
|
||||||
|
});
|
||||||
|
const [isFocused, focusProps] = useFocus();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onSelectionChange() {
|
||||||
|
const selection = document.getSelection();
|
||||||
|
|
||||||
|
if (selection) {
|
||||||
|
selectionRef.current = {
|
||||||
|
anchorNode: selection.anchorNode,
|
||||||
|
anchorOffset: selection.anchorOffset,
|
||||||
|
focusNode: selection.focusNode,
|
||||||
|
focusOffset: selection.focusOffset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFocused) {
|
||||||
|
document.addEventListener('selectionchange', onSelectionChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => document.removeEventListener('selectionchange', onSelectionChange);
|
||||||
|
}, [isFocused]);
|
||||||
|
|
||||||
|
const selectPreviousSelection = useCallback(() => {
|
||||||
|
setSelection(selectionRef.current);
|
||||||
|
}, [selectionRef]);
|
||||||
|
|
||||||
|
return { ...focusProps, selectPreviousSelection };
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/R
|
||||||
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||||
import { focusComposer } from "./utils";
|
import { focusComposer } from "./utils";
|
||||||
import { ComposerFunctions } from "../types";
|
import { ComposerFunctions } from "../types";
|
||||||
|
import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
|
||||||
export function useWysiwygSendActionHandler(
|
export function useWysiwygSendActionHandler(
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
|
@ -48,7 +49,18 @@ export function useWysiwygSendActionHandler(
|
||||||
composerFunctions.clear();
|
composerFunctions.clear();
|
||||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
break;
|
break;
|
||||||
// TODO: case Action.ComposerInsert: - see SendMessageComposer
|
case Action.ComposerInsert:
|
||||||
|
if (payload.timelineRenderingType !== roomContext.timelineRenderingType) break;
|
||||||
|
if (payload.composerType !== ComposerType.Send) break;
|
||||||
|
|
||||||
|
if (payload.userId) {
|
||||||
|
// TODO insert mention - see SendMessageComposer
|
||||||
|
} else if (payload.event) {
|
||||||
|
// TODO insert quote message - see SendMessageComposer
|
||||||
|
} else if (payload.text) {
|
||||||
|
composerFunctions.insertText(payload.text);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}, [disabled, composerElement, composerFunctions, timeoutId, roomContext]);
|
}, [disabled, composerElement, composerFunctions, timeoutId, roomContext]);
|
||||||
|
|
||||||
|
|
|
@ -16,4 +16,5 @@ limitations under the License.
|
||||||
|
|
||||||
export type ComposerFunctions = {
|
export type ComposerFunctions = {
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
|
insertText: (text: string) => void;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setSelection(selection:
|
||||||
|
Pick<Selection, 'anchorNode' | 'anchorOffset' | 'focusNode' | 'focusOffset'>,
|
||||||
|
) {
|
||||||
|
if (selection.anchorNode && selection.focusNode) {
|
||||||
|
const range = new Range();
|
||||||
|
range.setStart(selection.anchorNode, selection.anchorOffset);
|
||||||
|
range.setEnd(selection.focusNode, selection.focusOffset);
|
||||||
|
|
||||||
|
document.getSelection()?.removeAllRanges();
|
||||||
|
document.getSelection()?.addRange(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,6 +26,14 @@ import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||||
import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
|
import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
|
||||||
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
|
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
|
||||||
import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu";
|
import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu";
|
||||||
|
import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
import { setSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection";
|
||||||
|
|
||||||
|
jest.mock("../../../../../src/components/views/rooms/EmojiButton", () => ({
|
||||||
|
EmojiButton: ({ addEmoji }: {addEmoji: (emoji: string) => void}) => {
|
||||||
|
return <button aria-label="Emoji" type="button" onClick={() => addEmoji('🦫')}>Emoji</button>;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
describe('SendWysiwygComposer', () => {
|
describe('SendWysiwygComposer', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -47,9 +55,28 @@ describe('SendWysiwygComposer', () => {
|
||||||
|
|
||||||
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
|
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
|
||||||
|
|
||||||
|
const registerId = defaultDispatcher.register((payload) => {
|
||||||
|
switch (payload.action) {
|
||||||
|
case Action.ComposerInsert: {
|
||||||
|
if (payload.composerType) break;
|
||||||
|
|
||||||
|
// re-dispatch to the correct composer
|
||||||
|
defaultDispatcher.dispatch<ComposerInsertPayload>({
|
||||||
|
...(payload as ComposerInsertPayload),
|
||||||
|
composerType: ComposerType.Send,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
defaultDispatcher.unregister(registerId);
|
||||||
|
});
|
||||||
|
|
||||||
const customRender = (
|
const customRender = (
|
||||||
onChange = (_content: string) => void 0,
|
onChange = (_content: string): void => void 0,
|
||||||
onSend = () => void 0,
|
onSend = (): void => void 0,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
isRichTextEnabled = true,
|
isRichTextEnabled = true,
|
||||||
placeholder?: string) => {
|
placeholder?: string) => {
|
||||||
|
@ -177,7 +204,6 @@ describe('SendWysiwygComposer', () => {
|
||||||
|
|
||||||
it('Should not has placeholder', async () => {
|
it('Should not has placeholder', async () => {
|
||||||
// When
|
// When
|
||||||
console.log('here');
|
|
||||||
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
|
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
|
||||||
await waitFor(() => expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "true"));
|
await waitFor(() => expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "true"));
|
||||||
|
|
||||||
|
@ -222,5 +248,55 @@ describe('SendWysiwygComposer', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
{ isRichTextEnabled: true },
|
||||||
|
// TODO { isRichTextEnabled: false },
|
||||||
|
])('Emoji when %s', ({ isRichTextEnabled }) => {
|
||||||
|
let emojiButton: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
|
||||||
|
await waitFor(() => expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "true"));
|
||||||
|
emojiButton = screen.getByLabelText('Emoji');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should add an emoji in an empty composer', async () => {
|
||||||
|
// When
|
||||||
|
emojiButton.click();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
await waitFor(() => expect(screen.getByRole('textbox')).toHaveTextContent(/🦫/));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should add an emoji in the middle of a word', async () => {
|
||||||
|
// When
|
||||||
|
screen.getByRole('textbox').focus();
|
||||||
|
screen.getByRole('textbox').innerHTML = 'word';
|
||||||
|
fireEvent.input(screen.getByRole('textbox'), {
|
||||||
|
data: 'word',
|
||||||
|
inputType: 'insertText',
|
||||||
|
});
|
||||||
|
|
||||||
|
const textNode = screen.getByRole('textbox').firstChild;
|
||||||
|
setSelection({
|
||||||
|
anchorNode: textNode,
|
||||||
|
anchorOffset: 2,
|
||||||
|
focusNode: textNode,
|
||||||
|
focusOffset: 2,
|
||||||
|
});
|
||||||
|
// the event is not automatically fired by jest
|
||||||
|
document.dispatchEvent(new CustomEvent('selectionchange'));
|
||||||
|
|
||||||
|
emojiButton.click();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
await waitFor(() => expect(screen.getByRole('textbox')).toHaveTextContent(/wo🦫rd/));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1525,10 +1525,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/analytics-events/-/analytics-events-0.3.0.tgz#a428f7e3f164ffadf38f35bc0f0f9a3e47369ce6"
|
resolved "https://registry.yarnpkg.com/@matrix-org/analytics-events/-/analytics-events-0.3.0.tgz#a428f7e3f164ffadf38f35bc0f0f9a3e47369ce6"
|
||||||
integrity sha512-f1WIMA8tjNB3V5g1C34yIpIJK47z6IJ4SLiY4j+J9Gw4X8C3TKGTAx563rMcMvW3Uk/PFqnIBXtkavHBXoYJ9A==
|
integrity sha512-f1WIMA8tjNB3V5g1C34yIpIJK47z6IJ4SLiY4j+J9Gw4X8C3TKGTAx563rMcMvW3Uk/PFqnIBXtkavHBXoYJ9A==
|
||||||
|
|
||||||
"@matrix-org/matrix-wysiwyg@^0.8.0":
|
"@matrix-org/matrix-wysiwyg@^0.9.0":
|
||||||
version "0.8.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.8.0.tgz#3b64c6a16cf2027e395766c950c13752b1a81282"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.9.0.tgz#8651eacdc0bbfa313501e4feeb713c74dbf099cc"
|
||||||
integrity sha512-q3lpMNbD/GF2RPOuDR3COYDGR6BQWZBHUPtRYGaDf1i9eL/8vWD/WruwjzpI/RwNbYyPDm9Cs6vZj9BNhHB3Jw==
|
integrity sha512-utxLZPSmBR/oKFeLLteAfqprhSW8prrH9IKzeMK1VswQYganPusYYO8u86kCQt4SuDz/1Zc8C7r76xmOiVJ9JQ==
|
||||||
|
|
||||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz":
|
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz":
|
||||||
version "3.2.8"
|
version "3.2.8"
|
||||||
|
|
Loading…
Reference in a new issue