Add wysisyg composer (can only send message, enable behind a labs flag)
This commit is contained in:
parent
16c3efead5
commit
bfb1638ff3
11 changed files with 401 additions and 15 deletions
|
@ -94,6 +94,7 @@
|
|||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.1.1",
|
||||
"matrix-wysiwyg": "link:../matrix-wysiwyg/platforms/web",
|
||||
"minimist": "^1.2.5",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
|
|
|
@ -58,6 +58,7 @@ import {
|
|||
startNewVoiceBroadcastRecording,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from '../../../voice-broadcast';
|
||||
import { WysiwygComposer } from './wysiwyg_composer/WysiwygComposer';
|
||||
|
||||
let instanceCount = 0;
|
||||
|
||||
|
@ -105,6 +106,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
private composerSendMessage?: () => void;
|
||||
|
||||
private _voiceRecording: Optional<VoiceMessageRecording>;
|
||||
|
||||
|
@ -313,6 +315,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
this.messageComposerInput.current?.sendMessage();
|
||||
this.composerSendMessage?.();
|
||||
};
|
||||
|
||||
private onChange = (model: EditorModel) => {
|
||||
|
@ -321,6 +324,13 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onWysiwygChange = (content: string) => {
|
||||
console.log('content', content);
|
||||
this.setState({
|
||||
isComposerEmpty: content?.length === 0,
|
||||
});
|
||||
};
|
||||
|
||||
private onVoiceStoreUpdate = () => {
|
||||
this.updateRecordingState();
|
||||
};
|
||||
|
@ -394,20 +404,37 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
|
||||
if (canSendMessages) {
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
ref={this.messageComposerInput}
|
||||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
disabled={this.state.haveRecording}
|
||||
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
|
||||
/>,
|
||||
);
|
||||
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
|
||||
|
||||
if (isWysiwygComposerEnabled) {
|
||||
controls.push(
|
||||
<WysiwygComposer key="controls_input"
|
||||
disabled={this.state.haveRecording}
|
||||
onChange={this.onWysiwygChange}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent}>
|
||||
{ (sendMessage) => {
|
||||
this.composerSendMessage = sendMessage;
|
||||
} }
|
||||
</WysiwygComposer>,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
ref={this.messageComposerInput}
|
||||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
disabled={this.state.haveRecording}
|
||||
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
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, { useCallback, useState } from 'react';
|
||||
import { useWysiwyg } from "matrix-wysiwyg";
|
||||
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import { useRoomContext } from '../../../../contexts/RoomContext';
|
||||
import { sendMessage } from './message';
|
||||
import { RoomPermalinkCreator } from '../../../../utils/permalinks/Permalinks';
|
||||
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
|
||||
|
||||
interface WysiwygProps {
|
||||
disabled?: boolean;
|
||||
onChange: (content: string) => void;
|
||||
relation: IEventRelation;
|
||||
replyToEvent?: MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
includeReplyLegacyFallback?: boolean;
|
||||
children?: (sendMessage: () => void) => void;
|
||||
}
|
||||
|
||||
export function WysiwygComposer(
|
||||
{ disabled = false, onChange, children, ...props }: WysiwygProps, forwardRef,
|
||||
) {
|
||||
const roomContext = useRoomContext();
|
||||
const mxClient = useMatrixClientContext();
|
||||
|
||||
const [content, setContent] = useState<string>();
|
||||
const { ref, isWysiwygReady } = useWysiwyg({ onChange: (_content) => {
|
||||
setContent(_content);
|
||||
onChange(_content);
|
||||
} });
|
||||
|
||||
const memoizedSendMessage = useCallback(() => sendMessage(content, mxClient, { roomContext, ...props }),
|
||||
[content, mxClient, roomContext, props],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_SendMessageComposer" style={{ minHeight: '30px' }} ref={ref} contentEditable={!disabled && isWysiwygReady} suppressContentEditableWarning={true}>
|
||||
<br />
|
||||
{ children?.(memoizedSendMessage) }
|
||||
</div>
|
||||
);
|
||||
}
|
190
src/components/views/rooms/wysiwyg_composer/message.ts
Normal file
190
src/components/views/rooms/wysiwyg_composer/message.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
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 { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
||||
import { IContent, IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
|
||||
import { PosthogAnalytics } from "../../../../PosthogAnalytics";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../sendTimePerformanceMetrics";
|
||||
import { attachRelation } from "../SendMessageComposer";
|
||||
import { addReplyToMessageContent } from "../../../../utils/Reply";
|
||||
import { RoomPermalinkCreator } from "../../../../utils/permalinks/Permalinks";
|
||||
import { doMaybeLocalRoomAction } from "../../../../utils/local-room";
|
||||
import { CHAT_EFFECTS } from "../../../../effects";
|
||||
import { containsEmoji } from "../../../../effects/utils";
|
||||
import { IRoomState } from "../../../structures/RoomView";
|
||||
import dis from '../../../../dispatcher/dispatcher';
|
||||
|
||||
interface SendMessageParams {
|
||||
relation: IEventRelation;
|
||||
replyToEvent?: MatrixEvent;
|
||||
roomContext: IRoomState;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
includeReplyLegacyFallback?: boolean;
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export function createMessageContent(
|
||||
message: string,
|
||||
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true }:
|
||||
Omit<SendMessageParams, 'roomContext'>,
|
||||
): IContent {
|
||||
const isEmote = false;
|
||||
|
||||
// TODO do somethings about emote ?
|
||||
|
||||
/*const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
model = stripEmoteCommand(model);
|
||||
}
|
||||
if (startsWith(model, "//")) {
|
||||
model = stripPrefix(model, "/");
|
||||
}
|
||||
model = unescapeMessage(model);*/
|
||||
|
||||
// const body = textSerialize(model);
|
||||
const body = message;
|
||||
|
||||
const content: IContent = {
|
||||
msgtype: isEmote ? "m.emote" : "m.text",
|
||||
body: body,
|
||||
};
|
||||
|
||||
// TODO markdown support
|
||||
|
||||
/*const formattedBody = htmlSerializeIfNeeded(model, {
|
||||
forceHTML: !!replyToEvent,
|
||||
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
||||
});*/
|
||||
const formattedBody = message;
|
||||
|
||||
if (formattedBody) {
|
||||
content.format = "org.matrix.custom.html";
|
||||
content.formatted_body = formattedBody;
|
||||
}
|
||||
|
||||
attachRelation(content, relation);
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, {
|
||||
permalinkCreator,
|
||||
includeLegacyFallback: includeReplyLegacyFallback,
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
export function sendMessage(
|
||||
message: string,
|
||||
mxClient: MatrixClient,
|
||||
{ roomContext, ...params }: SendMessageParams,
|
||||
) {
|
||||
console.log('message', message);
|
||||
const { relation, replyToEvent } = params;
|
||||
const { room } = roomContext;
|
||||
const { roomId } = room;
|
||||
|
||||
const posthogEvent: ComposerEvent = {
|
||||
eventName: "Composer",
|
||||
isEditing: false,
|
||||
isReply: Boolean(replyToEvent),
|
||||
inThread: relation?.rel_type === THREAD_RELATION_TYPE.name,
|
||||
};
|
||||
if (posthogEvent.inThread) {
|
||||
const threadRoot = room.findEventById(relation.event_id);
|
||||
posthogEvent.startsThread = threadRoot?.getThread()?.events.length === 1;
|
||||
}
|
||||
PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
|
||||
|
||||
let content: IContent;
|
||||
|
||||
// TODO slash comment
|
||||
|
||||
// TODO replace emotion end of message ?
|
||||
|
||||
// TODO quick reaction
|
||||
|
||||
if (!content) {
|
||||
content = createMessageContent(
|
||||
message,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
// don't bother sending an empty message
|
||||
if (!content.body.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
decorateStartSendingTime(content);
|
||||
}
|
||||
|
||||
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name
|
||||
? relation.event_id
|
||||
: null;
|
||||
|
||||
const prom = doMaybeLocalRoomAction(
|
||||
roomId,
|
||||
(actualRoomId: string) => mxClient.sendMessage(actualRoomId, threadId, content),
|
||||
mxClient,
|
||||
);
|
||||
if (replyToEvent) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
context: roomContext.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
CHAT_EFFECTS.forEach((effect) => {
|
||||
if (containsEmoji(content, effect.emojis)) {
|
||||
// For initial threads launch, chat effects are disabled
|
||||
// see #19731
|
||||
const isNotThread = relation?.rel_type !== THREAD_RELATION_TYPE.name;
|
||||
if (!SettingsStore.getValue("feature_thread") || isNotThread) {
|
||||
dis.dispatch({ action: `effects.${effect.command}` });
|
||||
}
|
||||
}
|
||||
});
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
prom.then(resp => {
|
||||
sendRoundTripMetric(mxClient, roomId, resp.event_id);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO save history
|
||||
// TODO save local state
|
||||
|
||||
// this.sendHistoryManager.save(model, replyToEvent);
|
||||
// clear composer
|
||||
// model.reset([]);
|
||||
// this.editorRef.current?.clearUndoHistory();
|
||||
// this.editorRef.current?.focus();
|
||||
// this.clearStoredEditorState();
|
||||
//if (shouldSend && SettingsStore.getValue("scrollToBottomOnMessageSent")) {
|
||||
if (SettingsStore.getValue("scrollToBottomOnMessageSent")) {
|
||||
dis.dispatch({
|
||||
action: "scroll_to_bottom",
|
||||
timelineRenderingType: roomContext.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
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 { IEventRelation, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
export function useWysiwygStoredState(room: Room, relation: IEventRelation) {
|
||||
const editorStateKey = useMemo(() => {
|
||||
let key = `mx_cider_state_${room.roomId}`;
|
||||
if (relation?.rel_type === THREAD_RELATION_TYPE.name) {
|
||||
key += `_${relation.event_id}`;
|
||||
}
|
||||
return key;
|
||||
}, [room, relation]);
|
||||
|
||||
const clearStoredEditorState = useCallback(() => localStorage.removeItem(editorStateKey), [editorStateKey]);
|
||||
|
||||
return { clearStoredEditorState };
|
||||
}
|
|
@ -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 { DebouncedFunc, throttle } from "lodash";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
|
||||
export function useMatrixClient(room: Room) {
|
||||
const mxClient = useMatrixClientContext();
|
||||
|
||||
const [prepareToEncrypt, setPrepareToEncrypt] = useState<DebouncedFunc<() => void>>();
|
||||
useEffect(() => {
|
||||
if (mxClient.isCryptoEnabled() && mxClient.isRoomEncrypted(room.roomId)) {
|
||||
setPrepareToEncrypt(throttle(() => {
|
||||
mxClient.prepareToEncrypt(room);
|
||||
}, 60000, { leading: true, trailing: false }));
|
||||
}
|
||||
}, [mxClient, room]);
|
||||
|
||||
return { mxClient, prepareToEncrypt };
|
||||
}
|
|
@ -25,6 +25,10 @@ export interface MatrixClientProps {
|
|||
mxClient: MatrixClient;
|
||||
}
|
||||
|
||||
export function useMatrixClientContext() {
|
||||
return useContext(MatrixClientContext);
|
||||
}
|
||||
|
||||
const matrixHOC = <ComposedComponentProps extends {}>(
|
||||
ComposedComponent: ComponentClass<ComposedComponentProps>,
|
||||
) => {
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { createContext } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import { IRoomState } from "../components/structures/RoomView";
|
||||
import { Layout } from "../settings/enums/Layout";
|
||||
|
@ -68,3 +68,6 @@ const RoomContext = createContext<IRoomState>({
|
|||
});
|
||||
RoomContext.displayName = "RoomContext";
|
||||
export default RoomContext;
|
||||
export function useRoomContext() {
|
||||
return useContext(RoomContext);
|
||||
}
|
||||
|
|
|
@ -896,6 +896,7 @@
|
|||
"How can I leave the beta?": "How can I leave the beta?",
|
||||
"To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.",
|
||||
"Leave the beta": "Leave the beta",
|
||||
"Wysiwyg composer (under active development)": "Wysiwyg composer (under active development)",
|
||||
"Render simple counters in room header": "Render simple counters in room header",
|
||||
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
||||
"Support adding custom themes": "Support adding custom themes",
|
||||
|
|
|
@ -303,6 +303,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
},
|
||||
|
||||
},
|
||||
"feature_wysiwyg_composer": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Messaging,
|
||||
displayName: _td("Wysiwyg composer (under active development)"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"feature_state_counters": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Rooms,
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -6970,6 +6970,10 @@ matrix-widget-api@^1.1.1:
|
|||
"@types/events" "^3.0.0"
|
||||
events "^3.2.0"
|
||||
|
||||
"matrix-wysiwyg@link:../matrix-wysiwyg/platforms/web":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
mdurl@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
|
||||
|
@ -7943,6 +7947,14 @@ react-dom@17.0.2:
|
|||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-dom@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-focus-lock@^2.5.1:
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16"
|
||||
|
@ -8018,6 +8030,13 @@ react@17.0.2:
|
|||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
read-pkg-up@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
|
||||
|
@ -8403,6 +8422,13 @@ scheduler@^0.20.2:
|
|||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
scheduler@^0.23.0:
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
schema-utils@^3.0.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
|
||||
|
|
Loading…
Reference in a new issue