From d1bf2bee5df07113965efd58a8d2eb7284cb4c51 Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Thu, 26 Aug 2021 05:57:15 +0700 Subject: [PATCH 1/6] add basic support for attachments (as per MSC2881) Signed-off-by: Ivan Pavluk --- src/ContentMessages.tsx | 3 +- src/components/structures/RoomView.tsx | 18 ++++++++- .../views/messages/MessageEvent.tsx | 38 ++++++++++++++++++- src/components/views/rooms/EventTile.tsx | 3 ++ .../views/rooms/MessageComposer.tsx | 12 ++++-- .../views/rooms/SendMessageComposer.tsx | 31 +++++++++++++-- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 6 +++ 8 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 14a0c1ed517..985da301d76 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -430,7 +430,7 @@ export default class ContentMessages { } } - async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) { + async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient, promAfter?: (ISendEventResponse) => Promise) { if (matrixClient.isGuest()) { dis.dispatch({ action: 'require_registration' }); return; @@ -508,6 +508,7 @@ export default class ContentMessages { } promBefore = this.sendContentToRoom(file, roomId, matrixClient, promBefore); } + promBefore.then(promAfter); } getCurrentUploads() { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 474b99262da..c9ae71a7218 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -90,6 +90,7 @@ import MessageComposer from '../views/rooms/MessageComposer'; import JumpToBottomButton from "../views/rooms/JumpToBottomButton"; import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; import SpaceStore from "../../stores/SpaceStore"; +import { ISendEventResponse } from 'matrix-js-sdk/src/@types/requests'; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -203,6 +204,7 @@ export default class RoomView extends React.Component { private roomView = createRef(); private searchResultsPanel = createRef(); private messagePanel: TimelinePanel; + private messageComposer: MessageComposer; static contextType = MatrixClientContext; @@ -751,8 +753,12 @@ export default class RoomView extends React.Component { payload.data.description || payload.data.name); break; case 'picture_snapshot': + let promAfter = SettingsStore.getValue("feature_message_attachments") && this.messageComposer && !this.messageComposer.state.isComposerEmpty ? (event: ISendEventResponse) => { + return this.messageComposer.sendMessage(event.event_id); + } : null ContentMessages.sharedInstance().sendContentListToRoom( - [payload.file], this.state.room.roomId, this.context); + [payload.file], this.state.room.roomId, this.context, promAfter, + ); break; case 'notifier_enabled': case Action.UploadStarted: @@ -1245,8 +1251,11 @@ export default class RoomView extends React.Component { private onDrop = ev => { ev.stopPropagation(); ev.preventDefault(); + let promAfter = SettingsStore.getValue("feature_message_attachments") && this.messageComposer && ev.dataTransfer.files.length === 1 && !this.messageComposer.state.isComposerEmpty ? (event: ISendEventResponse) => { + return this.messageComposer.sendMessage(event.event_id); + } : null ContentMessages.sharedInstance().sendContentListToRoom( - ev.dataTransfer.files, this.state.room.roomId, this.context, + ev.dataTransfer.files, this.state.room.roomId, this.context, promAfter, ); dis.fire(Action.FocusSendMessageComposer); @@ -1684,6 +1693,10 @@ export default class RoomView extends React.Component { this.messagePanel = r; }; + private gatherMessageComposerRef = r => { + this.messageComposer = r; + }; + private getOldRoom() { const createEvent = this.state.room.currentState.getStateEvents("m.room.create", ""); if (!createEvent || !createEvent.getContent()['predecessor']) return null; @@ -1945,6 +1958,7 @@ export default class RoomView extends React.Component { if (canSpeak) { messageComposer = { @@ -37,15 +39,20 @@ interface IProps extends Omit { @replaceableComponent("views.messages.MessageEvent") export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { + public static contextType = MatrixClientContext; private body: React.RefObject = createRef(); private mediaHelper: MediaEventHelper; + private room: Room; - public constructor(props: IProps) { + public constructor(props: IProps, context: React.ContextType) { super(props); if (MediaEventHelper.isEligible(this.props.mxEvent)) { this.mediaHelper = new MediaEventHelper(this.props.mxEvent); } + + this.context = context; + this.room = this.context.getRoom(this.props.mxEvent.getRoomId()); } public componentWillUnmount() { @@ -127,9 +134,35 @@ export default class MessageEvent extends React.Component implements IMe } } } + + let attachment = null; + if (SettingsStore.getValue("feature_message_attachments")) { + if (this.props.mxEvent.isRelation("m.attachment")) { + let relation = this.props.mxEvent.getRelation(); + if (this.room && relation && relation.event_id) { + let event = this.room.findEventById(relation.event_id); + if (event) { + attachment = ( + + ); + } + } + } + } // @ts-ignore - this is a dynamic react component - return BodyType ? implements IMe permalinkCreator={this.props.permalinkCreator} mediaEventHelper={this.mediaHelper} /> : null; + return [body, attachment]; } } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index dd954e46ce6..5acf58d1121 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -55,6 +55,7 @@ import ReadReceiptMarker from "./ReadReceiptMarker"; import MessageActionBar from "../messages/MessageActionBar"; import ReactionsRow from '../messages/ReactionsRow'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; +import SettingsStore from "../../../settings/SettingsStore"; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -1209,6 +1210,8 @@ export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean) { return hasText(e, showHiddenEvents); } else if (handler === 'messages.RoomCreate') { return Boolean(e.getContent()['predecessor']); + } else if (SettingsStore.getValue("feature_message_attachments") && e.getContent()['is_attachment']) { + return false; } else { return true; } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 8455e9aa11c..24abff4b36d 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -45,6 +45,7 @@ import { Action } from "../../../dispatcher/actions"; import EditorModel from "../../../editor/model"; import EmojiPicker from '../emojipicker/EmojiPicker'; import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar"; +import { ISendEventResponse } from 'matrix-js-sdk/src/@types/requests'; interface IComposerAvatarProps { me: object; @@ -107,6 +108,7 @@ const EmojiButton = ({ addEmoji }) => { interface IUploadButtonProps { roomId: string; + composer?: MessageComposer; } class UploadButton extends React.Component { @@ -146,9 +148,13 @@ class UploadButton extends React.Component { for (let i = 0; i < ev.target.files.length; ++i) { tfiles.push(ev.target.files[i]); } + + let promAfter = SettingsStore.getValue("feature_message_attachments") && this.props.composer && ev.target.files.length === 1 && !this.props.composer.state.isComposerEmpty ? (event: ISendEventResponse) => { + return this.props.composer.sendMessage(event.event_id); + } : null; ContentMessages.sharedInstance().sendContentListToRoom( - tfiles, this.props.roomId, MatrixClientPeg.get(), + tfiles, this.props.roomId, MatrixClientPeg.get(), promAfter, ); // This is the onChange handler for a file form control, but we're @@ -324,7 +330,7 @@ export default class MessageComposer extends React.Component { }); } - private sendMessage = async () => { + public sendMessage = async (attachmentEventId?: string) => { if (this.state.haveRecording && this.voiceRecordingButton) { // There shouldn't be any text message to send when a voice recording is active, so // just send out the voice recording. @@ -332,7 +338,7 @@ export default class MessageComposer extends React.Component { return; } - this.messageComposerInput.sendMessage(); + this.messageComposerInput.sendMessage(attachmentEventId); }; private onChange = (model: EditorModel) => { diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 205320fb686..fcec7343671 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -54,6 +54,7 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import { ActionPayload } from "../../../dispatcher/payloads"; +import { ISendEventResponse } from 'matrix-js-sdk/src/@types/requests'; function addReplyToMessageContent( content: IContent, @@ -74,11 +75,25 @@ function addReplyToMessageContent( } } +function addAttachmentToMessageContent( + content: IContent, + attachEventId: string, +): void { + const attachContent = { + 'm.relates_to': { + 'rel_type': 'm.attachment', + 'event_id': attachEventId, + } + }; + Object.assign(content, attachContent); +} + // exported for tests export function createMessageContent( model: EditorModel, permalinkCreator: RoomPermalinkCreator, replyToEvent: MatrixEvent, + attachEventId: string, ): IContent { const isEmote = containsEmote(model); if (isEmote) { @@ -103,6 +118,10 @@ export function createMessageContent( if (replyToEvent) { addReplyToMessageContent(content, replyToEvent, permalinkCreator); } + + if (attachEventId) { + addAttachmentToMessageContent(content, attachEventId); + } return content; } @@ -342,7 +361,7 @@ export default class SendMessageComposer extends React.Component { } } - public async sendMessage(): Promise { + public async sendMessage(attachmentEventId?: string): Promise { if (this.model.isEmpty) { return; } @@ -359,6 +378,9 @@ export default class SendMessageComposer extends React.Component { if (replyToEvent) { addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator); } + if (attachmentEventId) { + addAttachmentToMessageContent(content, attachmentEventId); + } } else { this.runSlashCommand(cmd, args); shouldSend = false; @@ -400,7 +422,7 @@ export default class SendMessageComposer extends React.Component { const startTime = CountlyAnalytics.getTimestamp(); const { roomId } = this.props.room; if (!content) { - content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent); + content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent, attachmentEventId); } // don't bother sending an empty message if (!content.body.trim()) return; @@ -519,8 +541,11 @@ export default class SendMessageComposer extends React.Component { // We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer // it puts the filename in as text/plain which we want to ignore. if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) { + let promAfter = SettingsStore.getValue("feature_message_attachments") && clipboardData.files.length === 1 && !this.model.isEmpty ? (event: ISendEventResponse) => { + return this.sendMessage(event.event_id); + } : null; ContentMessages.sharedInstance().sendContentListToRoom( - Array.from(clipboardData.files), this.props.room.roomId, this.context, + Array.from(clipboardData.files), this.props.room.roomId, this.context, promAfter, ); return true; // to skip internal onPaste handler } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 21859fb1aaa..2a0ffd81552 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -819,6 +819,7 @@ "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms", "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", "Send pseudonymous analytics data": "Send pseudonymous analytics data", + "Message attachments": "Message attachments", "Show info about bridges in room settings": "Show info about bridges in room settings", "New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)", "Don't send read receipts": "Don't send read receipts", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 28c5b1353fc..1a297b35204 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -276,6 +276,12 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: false, controller: new PseudonymousAnalyticsController(), }, + "feature_message_attachments": { + isFeature: true, + supportedLevels: LEVELS_FEATURE, + displayName: _td('Message attachments'), + default: false, + }, "doNotDisturb": { supportedLevels: [SettingLevel.DEVICE], default: false, From dfd433bafc25cf5c082c91c0d0e1f67849d5a3a2 Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Thu, 26 Aug 2021 06:22:08 +0700 Subject: [PATCH 2/6] Fix is_attachment not being true for attachments --- src/ContentMessages.tsx | 14 +++++++++----- src/components/views/rooms/EventTile.tsx | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 985da301d76..a9f98bc306b 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -66,6 +66,7 @@ interface IContent { }; file?: string; url?: string; + is_attachment?: boolean; } interface IThumbnail { @@ -430,7 +431,7 @@ export default class ContentMessages { } } - async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient, promAfter?: (ISendEventResponse) => Promise) { + async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient, attachToMessage?: (ISendEventResponse) => Promise) { if (matrixClient.isGuest()) { dis.dispatch({ action: 'require_registration' }); return; @@ -506,9 +507,9 @@ export default class ContentMessages { uploadAll = true; } } - promBefore = this.sendContentToRoom(file, roomId, matrixClient, promBefore); + promBefore = this.sendContentToRoom(file, roomId, matrixClient, promBefore, Boolean(attachToMessage)); } - promBefore.then(promAfter); + promBefore.then(attachToMessage); } getCurrentUploads() { @@ -530,9 +531,9 @@ export default class ContentMessages { } } - private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise) { + private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise, attachment: boolean) { const startTime = CountlyAnalytics.getTimestamp(); - const content: IContent = { + let content: IContent = { body: file.name || 'Attachment', info: { size: file.size, @@ -540,6 +541,9 @@ export default class ContentMessages { msgtype: "", // set later }; + if (attachment) + content.is_attachment = true; + // if we have a mime type for the file, add it to the message metadata if (file.type) { content.info.mimetype = file.type; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 5acf58d1121..051876ce073 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1210,7 +1210,7 @@ export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean) { return hasText(e, showHiddenEvents); } else if (handler === 'messages.RoomCreate') { return Boolean(e.getContent()['predecessor']); - } else if (SettingsStore.getValue("feature_message_attachments") && e.getContent()['is_attachment']) { + } else if (SettingsStore.getValue("feature_message_attachments") && e.getContent().is_attachment) { return false; } else { return true; From f3157df808da679ee8488713ea60e0b2d7f5edd7 Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Thu, 26 Aug 2021 06:37:07 +0700 Subject: [PATCH 3/6] Fix eslint --- src/ContentMessages.tsx | 9 +++++++-- src/components/structures/RoomView.tsx | 16 ++++++++++------ src/components/views/messages/MessageEvent.tsx | 8 ++++---- src/components/views/rooms/MessageComposer.tsx | 10 ++++++---- .../views/rooms/SendMessageComposer.tsx | 16 ++++++++++------ 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index a9f98bc306b..2c89421f5b7 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -66,6 +66,7 @@ interface IContent { }; file?: string; url?: string; + // eslint-disable-next-line camelcase is_attachment?: boolean; } @@ -431,7 +432,10 @@ export default class ContentMessages { } } - async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient, attachToMessage?: (ISendEventResponse) => Promise) { + async sendContentListToRoom( + files: File[], roomId: string, matrixClient: MatrixClient, + attachToMessage?: (ISendEventResponse) => Promise + ) { if (matrixClient.isGuest()) { dis.dispatch({ action: 'require_registration' }); return; @@ -541,8 +545,9 @@ export default class ContentMessages { msgtype: "", // set later }; - if (attachment) + if (attachment) { content.is_attachment = true; + } // if we have a mime type for the file, add it to the message metadata if (file.type) { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index c9ae71a7218..77ff44dc500 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -753,9 +753,11 @@ export default class RoomView extends React.Component { payload.data.description || payload.data.name); break; case 'picture_snapshot': - let promAfter = SettingsStore.getValue("feature_message_attachments") && this.messageComposer && !this.messageComposer.state.isComposerEmpty ? (event: ISendEventResponse) => { - return this.messageComposer.sendMessage(event.event_id); - } : null + const promAfter = (SettingsStore.getValue("feature_message_attachments") && this.messageComposer + && !this.messageComposer.state.isComposerEmpty) ? + (event: ISendEventResponse) => { + return this.messageComposer.sendMessage(event.event_id); + } : null; ContentMessages.sharedInstance().sendContentListToRoom( [payload.file], this.state.room.roomId, this.context, promAfter, ); @@ -1251,9 +1253,11 @@ export default class RoomView extends React.Component { private onDrop = ev => { ev.stopPropagation(); ev.preventDefault(); - let promAfter = SettingsStore.getValue("feature_message_attachments") && this.messageComposer && ev.dataTransfer.files.length === 1 && !this.messageComposer.state.isComposerEmpty ? (event: ISendEventResponse) => { - return this.messageComposer.sendMessage(event.event_id); - } : null + let promAfter = (SettingsStore.getValue("feature_message_attachments") && this.messageComposer + && ev.dataTransfer.files.length === 1 && !this.messageComposer.state.isComposerEmpty) ? + (event: ISendEventResponse) => { + return this.messageComposer.sendMessage(event.event_id); + } : null ContentMessages.sharedInstance().sendContentListToRoom( ev.dataTransfer.files, this.state.room.roomId, this.context, promAfter, ); diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 203dde21929..b398ccee448 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -134,13 +134,13 @@ export default class MessageEvent extends React.Component implements IMe } } } - + let attachment = null; if (SettingsStore.getValue("feature_message_attachments")) { if (this.props.mxEvent.isRelation("m.attachment")) { - let relation = this.props.mxEvent.getRelation(); + const relation = this.props.mxEvent.getRelation(); if (this.room && relation && relation.event_id) { - let event = this.room.findEventById(relation.event_id); + const event = this.room.findEventById(relation.event_id); if (event) { attachment = ( implements IMe } // @ts-ignore - this is a dynamic react component - let body = BodyType ? { for (let i = 0; i < ev.target.files.length; ++i) { tfiles.push(ev.target.files[i]); } - - let promAfter = SettingsStore.getValue("feature_message_attachments") && this.props.composer && ev.target.files.length === 1 && !this.props.composer.state.isComposerEmpty ? (event: ISendEventResponse) => { - return this.props.composer.sendMessage(event.event_id); - } : null; + + const promAfter = (SettingsStore.getValue("feature_message_attachments") && this.props.composer + && ev.target.files.length === 1 && !this.props.composer.state.isComposerEmpty) ? + (event: ISendEventResponse) => { + return this.props.composer.sendMessage(event.event_id); + } : null; ContentMessages.sharedInstance().sendContentListToRoom( tfiles, this.props.roomId, MatrixClientPeg.get(), promAfter, diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index fcec7343671..20843fc4bb1 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -83,7 +83,7 @@ function addAttachmentToMessageContent( 'm.relates_to': { 'rel_type': 'm.attachment', 'event_id': attachEventId, - } + }, }; Object.assign(content, attachContent); } @@ -118,7 +118,7 @@ export function createMessageContent( if (replyToEvent) { addReplyToMessageContent(content, replyToEvent, permalinkCreator); } - + if (attachEventId) { addAttachmentToMessageContent(content, attachEventId); } @@ -422,7 +422,9 @@ export default class SendMessageComposer extends React.Component { const startTime = CountlyAnalytics.getTimestamp(); const { roomId } = this.props.room; if (!content) { - content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent, attachmentEventId); + content = createMessageContent( + this.model, this.props.permalinkCreator, replyToEvent, attachmentEventId, + ); } // don't bother sending an empty message if (!content.body.trim()) return; @@ -541,9 +543,11 @@ export default class SendMessageComposer extends React.Component { // We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer // it puts the filename in as text/plain which we want to ignore. if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) { - let promAfter = SettingsStore.getValue("feature_message_attachments") && clipboardData.files.length === 1 && !this.model.isEmpty ? (event: ISendEventResponse) => { - return this.sendMessage(event.event_id); - } : null; + const promAfter = (SettingsStore.getValue("feature_message_attachents") + && clipboardData.files.length === 1 && !this.model.isEmpty) ? + (event: ISendEventResponse) => { + return this.sendMessage(event.event_id); + } : null; ContentMessages.sharedInstance().sendContentListToRoom( Array.from(clipboardData.files), this.props.room.roomId, this.context, promAfter, ); From 94bb9a5ff639cc2d31663f58eab846746a963f23 Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Thu, 26 Aug 2021 06:41:55 +0700 Subject: [PATCH 4/6] Fix eslint... --- src/ContentMessages.tsx | 8 +++++--- src/components/structures/RoomView.tsx | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 2c89421f5b7..99be5174f6d 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -434,7 +434,7 @@ export default class ContentMessages { async sendContentListToRoom( files: File[], roomId: string, matrixClient: MatrixClient, - attachToMessage?: (ISendEventResponse) => Promise + attachToMessage?: (ISendEventResponse) => Promise, ) { if (matrixClient.isGuest()) { dis.dispatch({ action: 'require_registration' }); @@ -535,9 +535,11 @@ export default class ContentMessages { } } - private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise, attachment: boolean) { + private sendContentToRoom( + file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise, attachment: boolean, + ) { const startTime = CountlyAnalytics.getTimestamp(); - let content: IContent = { + const content: IContent = { body: file.name || 'Attachment', info: { size: file.size, diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 77ff44dc500..03170584864 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -752,7 +752,7 @@ export default class RoomView extends React.Component { payload.data.content.info, payload.data.description || payload.data.name); break; - case 'picture_snapshot': + case 'picture_snapshot': { const promAfter = (SettingsStore.getValue("feature_message_attachments") && this.messageComposer && !this.messageComposer.state.isComposerEmpty) ? (event: ISendEventResponse) => { @@ -762,6 +762,7 @@ export default class RoomView extends React.Component { [payload.file], this.state.room.roomId, this.context, promAfter, ); break; + } case 'notifier_enabled': case Action.UploadStarted: case Action.UploadFinished: @@ -1253,11 +1254,11 @@ export default class RoomView extends React.Component { private onDrop = ev => { ev.stopPropagation(); ev.preventDefault(); - let promAfter = (SettingsStore.getValue("feature_message_attachments") && this.messageComposer + const promAfter = (SettingsStore.getValue("feature_message_attachments") && this.messageComposer && ev.dataTransfer.files.length === 1 && !this.messageComposer.state.isComposerEmpty) ? (event: ISendEventResponse) => { return this.messageComposer.sendMessage(event.event_id); - } : null + } : null; ContentMessages.sharedInstance().sendContentListToRoom( ev.dataTransfer.files, this.state.room.roomId, this.context, promAfter, ); From d489ba706f4f4d0e620f1d5b27b9e16448dd83c7 Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Thu, 26 Aug 2021 07:23:11 +0700 Subject: [PATCH 5/6] Use unstable prefix for m.attachment --- src/components/views/messages/MessageEvent.tsx | 2 +- src/components/views/rooms/SendMessageComposer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index b398ccee448..f24c4ba040a 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -137,7 +137,7 @@ export default class MessageEvent extends React.Component implements IMe let attachment = null; if (SettingsStore.getValue("feature_message_attachments")) { - if (this.props.mxEvent.isRelation("m.attachment")) { + if (this.props.mxEvent.isRelation("org.matrix.msc2881.m.attachment")) { const relation = this.props.mxEvent.getRelation(); if (this.room && relation && relation.event_id) { const event = this.room.findEventById(relation.event_id); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 20843fc4bb1..7519729e53a 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -81,7 +81,7 @@ function addAttachmentToMessageContent( ): void { const attachContent = { 'm.relates_to': { - 'rel_type': 'm.attachment', + 'rel_type': 'org.matrix.msc2881.m.attachment', 'event_id': attachEventId, }, }; From 108b0a78280081ba26595c831a2549e69622e413 Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Thu, 26 Aug 2021 10:28:32 +0700 Subject: [PATCH 6/6] Allow replying to messages with files (temporarily without m.relates_to) --- src/ContentMessages.tsx | 3 ++- src/components/structures/RoomView.tsx | 5 +++-- .../views/rooms/MessageComposer.tsx | 3 ++- .../views/rooms/SendMessageComposer.tsx | 19 ++++++++++++++----- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 99be5174f6d..3d9b29ac154 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -39,6 +39,7 @@ import { import { IUpload } from "./models/IUpload"; import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; import { BlurhashEncoder } from "./BlurhashEncoder"; +import SettingsStore from "./settings/SettingsStore"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -442,7 +443,7 @@ export default class ContentMessages { } const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); - if (isQuoting) { + if (isQuoting && !SettingsStore.getValue("feature_message_attachments")) { // FIXME: Using an import will result in Element crashing const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 03170584864..7ca9f8c09d3 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -754,7 +754,7 @@ export default class RoomView extends React.Component { break; case 'picture_snapshot': { const promAfter = (SettingsStore.getValue("feature_message_attachments") && this.messageComposer - && !this.messageComposer.state.isComposerEmpty) ? + && (!this.messageComposer.state.isComposerEmpty || this.messageComposer.props.replyToEvent)) ? (event: ISendEventResponse) => { return this.messageComposer.sendMessage(event.event_id); } : null; @@ -1255,7 +1255,8 @@ export default class RoomView extends React.Component { ev.stopPropagation(); ev.preventDefault(); const promAfter = (SettingsStore.getValue("feature_message_attachments") && this.messageComposer - && ev.dataTransfer.files.length === 1 && !this.messageComposer.state.isComposerEmpty) ? + && ev.dataTransfer.files.length === 1 + && (!this.messageComposer.state.isComposerEmpty || this.messageComposer.props.replyToEvent)) ? (event: ISendEventResponse) => { return this.messageComposer.sendMessage(event.event_id); } : null; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 07cc465f8f9..b57afc5c434 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -150,7 +150,8 @@ class UploadButton extends React.Component { } const promAfter = (SettingsStore.getValue("feature_message_attachments") && this.props.composer - && ev.target.files.length === 1 && !this.props.composer.state.isComposerEmpty) ? + && ev.target.files.length === 1 + && (!this.props.composer.state.isComposerEmpty || this.props.composer.props.replyToEvent)) ? (event: ISendEventResponse) => { return this.props.composer.sendMessage(event.event_id); } : null; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 7519729e53a..90830fba913 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -55,6 +55,7 @@ import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import { ActionPayload } from "../../../dispatcher/payloads"; import { ISendEventResponse } from 'matrix-js-sdk/src/@types/requests'; +import DocumentOffset from '../../../editor/offset'; function addReplyToMessageContent( content: IContent, @@ -110,15 +111,18 @@ export function createMessageContent( body: body, }; const formattedBody = htmlSerializeIfNeeded(model, { forceHTML: !!replyToEvent }); - if (formattedBody) { + if (formattedBody || replyToEvent) { content.format = "org.matrix.custom.html"; - content.formatted_body = formattedBody; + content.formatted_body = formattedBody || body; } if (replyToEvent) { addReplyToMessageContent(content, replyToEvent, permalinkCreator); } + // TODO: Currently, an attachment will override a reply. + // This allows replying with images, but removes the reply relation from the message. + // When/if we get the ability to add multiple relations, this will be fixed. if (attachEventId) { addAttachmentToMessageContent(content, attachEventId); } @@ -363,7 +367,12 @@ export default class SendMessageComposer extends React.Component { public async sendMessage(attachmentEventId?: string): Promise { if (this.model.isEmpty) { - return; + if (!attachmentEventId) { + return; + } + // If replying with just an attachment, add empty text to model so it has at least one part. + // Otherwise, various functions expecting at least one part will fail. + this.model.update(" ", "insertText", new DocumentOffset(1, true)); } const replyToEvent = this.props.replyToEvent; @@ -543,8 +552,8 @@ export default class SendMessageComposer extends React.Component { // We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer // it puts the filename in as text/plain which we want to ignore. if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) { - const promAfter = (SettingsStore.getValue("feature_message_attachents") - && clipboardData.files.length === 1 && !this.model.isEmpty) ? + const promAfter = (SettingsStore.getValue("feature_message_attachments") + && clipboardData.files.length === 1 && (!this.model.isEmpty || this.props.replyToEvent)) ? (event: ISendEventResponse) => { return this.sendMessage(event.event_id); } : null;