-
-
Notifications
You must be signed in to change notification settings - Fork 827
Add basic support for attachments (as per MSC2881) #6683
Changes from all commits
d1bf2be
dfd433b
f3157df
94bb9a5
d489ba7
108b0a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<IProps, IState> { | |
private roomView = createRef<HTMLElement>(); | ||
private searchResultsPanel = createRef<ScrollPanel>(); | ||
private messagePanel: TimelinePanel; | ||
private messageComposer: MessageComposer; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that |
||
|
||
static contextType = MatrixClientContext; | ||
|
||
|
@@ -750,10 +752,17 @@ export default class RoomView extends React.Component<IProps, IState> { | |
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 || this.messageComposer.props.replyToEvent)) ? | ||
(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: | ||
case Action.UploadFinished: | ||
|
@@ -1245,8 +1254,14 @@ export default class RoomView extends React.Component<IProps, IState> { | |
private onDrop = ev => { | ||
ev.stopPropagation(); | ||
ev.preventDefault(); | ||
const promAfter = (SettingsStore.getValue("feature_message_attachments") && this.messageComposer | ||
&& ev.dataTransfer.files.length === 1 | ||
&& (!this.messageComposer.state.isComposerEmpty || this.messageComposer.props.replyToEvent)) ? | ||
(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 +1699,10 @@ export default class RoomView extends React.Component<IProps, IState> { | |
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 +1964,7 @@ export default class RoomView extends React.Component<IProps, IState> { | |
if (canSpeak) { | ||
messageComposer = | ||
<MessageComposer | ||
ref={this.gatherMessageComposerRef} | ||
room={this.state.room} | ||
e2eStatus={this.state.e2eStatus} | ||
resizeNotifier={this.props.resizeNotifier} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<IUploadButtonProps> { | ||
|
@@ -147,8 +149,15 @@ class UploadButton extends React.Component<IUploadButtonProps> { | |
tfiles.push(ev.target.files[i]); | ||
} | ||
|
||
const promAfter = (SettingsStore.getValue("feature_message_attachments") && this.props.composer | ||
&& 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; | ||
Comment on lines
+152
to
+157
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would be much more readable if it were split into variables |
||
|
||
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,15 +333,15 @@ export default class MessageComposer extends React.Component<IProps, IState> { | |
}); | ||
} | ||
|
||
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. | ||
await this.voiceRecordingButton.send(); | ||
return; | ||
} | ||
|
||
this.messageComposerInput.sendMessage(); | ||
this.messageComposerInput.sendMessage(attachmentEventId); | ||
}; | ||
|
||
private onChange = (model: EditorModel) => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,8 @@ 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'; | ||
import DocumentOffset from '../../../editor/offset'; | ||
|
||
function addReplyToMessageContent( | ||
content: IContent, | ||
|
@@ -74,11 +76,25 @@ function addReplyToMessageContent( | |
} | ||
} | ||
|
||
function addAttachmentToMessageContent( | ||
content: IContent, | ||
attachEventId: string, | ||
): void { | ||
const attachContent = { | ||
'm.relates_to': { | ||
'rel_type': 'org.matrix.msc2881.m.attachment', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. throughout we generally prefer to use There should be some example usages in the code somewhere: look for |
||
'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) { | ||
|
@@ -95,15 +111,22 @@ 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); | ||
} | ||
|
||
return content; | ||
} | ||
|
||
|
@@ -342,9 +365,14 @@ export default class SendMessageComposer extends React.Component<IProps> { | |
} | ||
} | ||
|
||
public async sendMessage(): Promise<void> { | ||
public async sendMessage(attachmentEventId?: string): Promise<void> { | ||
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; | ||
|
@@ -359,6 +387,9 @@ export default class SendMessageComposer extends React.Component<IProps> { | |
if (replyToEvent) { | ||
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator); | ||
} | ||
if (attachmentEventId) { | ||
addAttachmentToMessageContent(content, attachmentEventId); | ||
} | ||
} else { | ||
this.runSlashCommand(cmd, args); | ||
shouldSend = false; | ||
|
@@ -400,7 +431,9 @@ export default class SendMessageComposer extends React.Component<IProps> { | |
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 +552,13 @@ export default class SendMessageComposer extends React.Component<IProps> { | |
// 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_attachments") | ||
&& clipboardData.files.length === 1 && (!this.model.isEmpty || this.props.replyToEvent)) ? | ||
(event: ISendEventResponse) => { | ||
return this.sendMessage(event.event_id); | ||
} : null; | ||
Comment on lines
+555
to
+559
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would be more readable if it were split into variables |
||
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 | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to avoid the callback here? It would be more readable, imo