Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/feat/widgets

 Conflicts:
	src/components/views/elements/AppTile.js
This commit is contained in:
Michael Telatynski
2020-10-09 08:45:11 +01:00
14 changed files with 1715 additions and 736 deletions

View File

@@ -16,12 +16,21 @@ limitations under the License.
*/
import {clamp} from "lodash";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {SerializedPart} from "./editor/parts";
import EditorModel from "./editor/model";
interface IHistoryItem {
parts: SerializedPart[];
replyEventId?: string;
}
export default class SendHistoryManager {
history: Array<HistoryItem> = [];
history: Array<IHistoryItem> = [];
prefix: string;
lastIndex: number = 0; // used for indexing the storage
currentIndex: number = 0; // used for indexing the loaded validated history Array
lastIndex = 0; // used for indexing the storage
currentIndex = 0; // used for indexing the loaded validated history Array
constructor(roomId: string, prefix: string) {
this.prefix = prefix + roomId;
@@ -32,8 +41,7 @@ export default class SendHistoryManager {
while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) {
try {
const serializedParts = JSON.parse(itemJSON);
this.history.push(serializedParts);
this.history.push(JSON.parse(itemJSON));
} catch (e) {
console.warn("Throwing away unserialisable history", e);
break;
@@ -45,15 +53,22 @@ export default class SendHistoryManager {
this.currentIndex = this.lastIndex + 1;
}
save(editorModel: Object) {
const serializedParts = editorModel.serializeParts();
this.history.push(serializedParts);
this.currentIndex = this.history.length;
this.lastIndex += 1;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(serializedParts));
static createItem(model: EditorModel, replyEvent?: MatrixEvent): IHistoryItem {
return {
parts: model.serializeParts(),
replyEventId: replyEvent ? replyEvent.getId() : undefined,
};
}
getItem(offset: number): ?HistoryItem {
save(editorModel: EditorModel, replyEvent?: MatrixEvent) {
const item = SendHistoryManager.createItem(editorModel, replyEvent);
this.history.push(item);
this.currentIndex = this.history.length;
this.lastIndex += 1;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(item));
}
getItem(offset: number): IHistoryItem {
this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1);
return this.history[this.currentIndex];
}

View File

@@ -166,7 +166,8 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
const onKeyDownHandler = useCallback((ev) => {
let handled = false;
if (handleHomeEnd) {
// Don't interfere with input default keydown behaviour
if (handleHomeEnd && ev.target.tagName !== "INPUT") {
// check if we actually have any items
switch (ev.key) {
case Key.HOME:

View File

@@ -28,6 +28,9 @@ interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
const Toolbar: React.FC<IProps> = ({children, ...props}) => {
const onKeyDown = (ev: React.KeyboardEvent, state: IState) => {
const target = ev.target as HTMLElement;
// Don't interfere with input default keydown behaviour
if (target.tagName === "INPUT") return;
let handled = true;
// HOME and END are handled by RovingTabIndexProvider

View File

@@ -47,6 +47,7 @@ export default class AppTile extends React.Component {
// The key used for PersistedElement
this._persistKey = getPersistKey(this.props.app.id);
this._sgWidget = new StopGapWidget(this.props);
this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady);
this.iframe = null; // ref to the iframe (callback style)
@@ -95,9 +96,7 @@ export default class AppTile extends React.Component {
this._sgWidget.stop();
}
this.setState({
hasPermissionToLoad,
});
this.setState({ hasPermissionToLoad });
};
isMixedContent() {
@@ -144,6 +143,7 @@ export default class AppTile extends React.Component {
this._sgWidget.stop();
}
this._sgWidget = new StopGapWidget(newProps);
this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady);
this._startWidget();
}
@@ -204,8 +204,11 @@ export default class AppTile extends React.Component {
this._sgWidget.stop();
}
_onWidgetReady = () => {
_onWidgetPrepared = () => {
this.setState({loading: false});
};
_onWidgetReady = () => {
if (WidgetType.JITSI.matches(this.props.app.type)) {
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
}

View File

@@ -80,27 +80,30 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) {
// Fake it till we make it
const event = new MatrixEvent(JSON.parse(`{
"type": "m.room.message",
"sender": "${userId}",
"content": {
"m.new_content": {
"msgtype": "m.text",
"body": "${this.props.message}",
"displayname": "${displayname}",
"avatar_url": "${avatarUrl}"
},
"msgtype": "m.text",
"body": "${this.props.message}",
"displayname": "${displayname}",
"avatar_url": "${avatarUrl}"
/* eslint-disable quote-props */
const rawEvent = {
type: "m.room.message",
sender: userId,
content: {
"m.new_content": {
msgtype: "m.text",
body: this.props.message,
displayname: displayname,
avatar_url: avatarUrl,
},
"unsigned": {
"age": 97
},
"event_id": "$9999999999999999999999999999999999999999999",
"room_id": "!999999999999999999:matrix.org"
}`));
msgtype: "m.text",
body: this.props.message,
displayname: displayname,
avatar_url: avatarUrl,
},
unsigned: {
age: 97,
},
event_id: "$9999999999999999999999999999999999999999999",
room_id: "!999999999999999999:example.org",
};
const event = new MatrixEvent(rawEvent);
/* eslint-enable quote-props */
// Fake it more
event.sender = {

View File

@@ -92,7 +92,7 @@ interface IProps {
label?: string;
initialCaret?: DocumentOffset;
onChange();
onChange?();
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
}

View File

@@ -257,7 +257,7 @@ export default class MessageComposer extends React.Component {
this._dispatcherRef = null;
this.state = {
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
replyToEvent: RoomViewStore.getQuotingEvent(),
tombstone: this._getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(),
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
@@ -337,9 +337,9 @@ export default class MessageComposer extends React.Component {
}
_onRoomViewStoreUpdate() {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (this.state.isQuoting === isQuoting) return;
this.setState({ isQuoting });
const replyToEvent = RoomViewStore.getQuotingEvent();
if (this.state.replyToEvent === replyToEvent) return;
this.setState({ replyToEvent });
}
onInputStateChanged(inputState) {
@@ -378,7 +378,7 @@ export default class MessageComposer extends React.Component {
}
renderPlaceholderText() {
if (this.state.isQuoting) {
if (this.state.replyToEvent) {
if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');
} else {
@@ -423,7 +423,9 @@ export default class MessageComposer extends React.Component {
room={this.props.room}
placeholder={this.renderPlaceholderText()}
resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.props.permalinkCreator} />,
permalinkCreator={this.props.permalinkCreator}
replyToEvent={this.state.replyToEvent}
/>,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
);

View File

@@ -29,7 +29,6 @@ import {
} from '../../../editor/serialize';
import {CommandPartCreator} from '../../../editor/parts';
import BasicMessageComposer from "./BasicMessageComposer";
import RoomViewStore from '../../../stores/RoomViewStore';
import ReplyThread from "../elements/ReplyThread";
import {parseEvent} from '../../../editor/deserialize';
import {findEditableEvent} from '../../../utils/EventUtils';
@@ -41,7 +40,6 @@ import {_t, _td} from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";
@@ -61,7 +59,7 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
}
// exported for tests
export function createMessageContent(model, permalinkCreator) {
export function createMessageContent(model, permalinkCreator, replyToEvent) {
const isEmote = containsEmote(model);
if (isEmote) {
model = stripEmoteCommand(model);
@@ -70,21 +68,20 @@ export function createMessageContent(model, permalinkCreator) {
model = stripPrefix(model, "/");
}
model = unescapeMessage(model);
const repliedToEvent = RoomViewStore.getQuotingEvent();
const body = textSerialize(model);
const content = {
msgtype: isEmote ? "m.emote" : "m.text",
body: body,
};
const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!repliedToEvent});
const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!replyToEvent});
if (formattedBody) {
content.format = "org.matrix.custom.html";
content.formatted_body = formattedBody;
}
if (repliedToEvent) {
addReplyToMessageContent(content, repliedToEvent, permalinkCreator);
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, permalinkCreator);
}
return content;
@@ -95,6 +92,7 @@ export default class SendMessageComposer extends React.Component {
room: PropTypes.object.isRequired,
placeholder: PropTypes.string,
permalinkCreator: PropTypes.object.isRequired,
replyToEvent: PropTypes.object,
};
static contextType = MatrixClientContext;
@@ -104,12 +102,13 @@ export default class SendMessageComposer extends React.Component {
this.model = null;
this._editorRef = null;
this.currentlyComposedEditorState = null;
const cli = MatrixClientPeg.get();
if (cli.isCryptoEnabled() && cli.isRoomEncrypted(this.props.room.roomId)) {
if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
this._prepareToEncrypt = new RateLimitedFunc(() => {
cli.prepareToEncrypt(this.props.room);
this.context.prepareToEncrypt(this.props.room);
}, 60000);
}
window.addEventListener("beforeunload", this._saveStoredEditorState);
}
_setEditorRef = ref => {
@@ -145,7 +144,7 @@ export default class SendMessageComposer extends React.Component {
if (e.shiftKey || e.metaKey) return;
const shouldSelectHistory = e.altKey && e.ctrlKey;
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !RoomViewStore.getQuotingEvent();
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !this.props.replyToEvent;
if (shouldSelectHistory) {
// Try select composer history
@@ -187,9 +186,13 @@ export default class SendMessageComposer extends React.Component {
this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length;
return;
}
const serializedParts = this.sendHistoryManager.getItem(delta);
if (serializedParts) {
this.model.reset(serializedParts);
const {parts, replyEventId} = this.sendHistoryManager.getItem(delta);
dis.dispatch({
action: 'reply_to_event',
event: replyEventId ? this.props.room.findEventById(replyEventId) : null,
});
if (parts) {
this.model.reset(parts);
this._editorRef.focus();
}
}
@@ -299,12 +302,12 @@ export default class SendMessageComposer extends React.Component {
}
}
const replyToEvent = this.props.replyToEvent;
if (shouldSend) {
const isReply = !!RoomViewStore.getQuotingEvent();
const {roomId} = this.props.room;
const content = createMessageContent(this.model, this.props.permalinkCreator);
const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
this.context.sendMessage(roomId, content);
if (isReply) {
if (replyToEvent) {
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
dis.dispatch({
@@ -315,7 +318,7 @@ export default class SendMessageComposer extends React.Component {
dis.dispatch({action: "message_sent"});
}
this.sendHistoryManager.save(this.model);
this.sendHistoryManager.save(this.model, replyToEvent);
// clear composer
this.model.reset([]);
this._editorRef.clearUndoHistory();
@@ -325,6 +328,8 @@ export default class SendMessageComposer extends React.Component {
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
window.removeEventListener("beforeunload", this._saveStoredEditorState);
this._saveStoredEditorState();
}
// TODO: [REACT-WARNING] Move this to constructor
@@ -333,11 +338,11 @@ export default class SendMessageComposer extends React.Component {
const parts = this._restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator);
this.dispatcherRef = dis.register(this.onAction);
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_composer_history_');
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_history_');
}
get _editorStateKey() {
return `cider_editor_state_${this.props.room.roomId}`;
return `mx_cider_state_${this.props.room.roomId}`;
}
_clearStoredEditorState() {
@@ -347,9 +352,19 @@ export default class SendMessageComposer extends React.Component {
_restoreStoredEditorState(partCreator) {
const json = localStorage.getItem(this._editorStateKey);
if (json) {
const serializedParts = JSON.parse(json);
const parts = serializedParts.map(p => partCreator.deserializePart(p));
return parts;
try {
const {parts: serializedParts, replyEventId} = JSON.parse(json);
const parts = serializedParts.map(p => partCreator.deserializePart(p));
if (replyEventId) {
dis.dispatch({
action: 'reply_to_event',
event: this.props.room.findEventById(replyEventId),
});
}
return parts;
} catch (e) {
console.error(e);
}
}
}
@@ -357,7 +372,8 @@ export default class SendMessageComposer extends React.Component {
if (this.model.isEmpty) {
this._clearStoredEditorState();
} else {
localStorage.setItem(this._editorStateKey, JSON.stringify(this.model.serializeParts()));
const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent);
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
}
}
@@ -449,7 +465,6 @@ export default class SendMessageComposer extends React.Component {
room={this.props.room}
label={this.props.placeholder}
placeholder={this.props.placeholder}
onChange={this._saveStoredEditorState}
onPaste={this._onPaste}
/>
</div>

View File

@@ -163,6 +163,7 @@ export class StopGapWidget extends EventEmitter {
if (this.started) return;
const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
this.messaging.addEventListener("preparing", () => this.emit("preparing"));
this.messaging.addEventListener("ready", () => this.emit("ready"));
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);