Merge branch 'develop' into travis/voice-messages/interrupt-text

This commit is contained in:
Travis Ralston 2021-09-02 11:21:12 -06:00
commit b2cb944d73
15 changed files with 82 additions and 31 deletions

View file

@ -354,7 +354,7 @@ $appearance-tab-border-color: $input-darker-bg-color;
// blur amounts for left left panel (only for element theme) // blur amounts for left left panel (only for element theme)
:root { :root {
--lp-background-blur: 30px; --lp-background-blur: 40px;
} }
$composer-shadow-color: rgba(0, 0, 0, 0.04); $composer-shadow-color: rgba(0, 0, 0, 0.04);

View file

@ -519,6 +519,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
inlineErrors: true, inlineErrors: true,
parentSpace: space, parentSpace: space,
joinRule: !isPublic ? JoinRule.Restricted : undefined, joinRule: !isPublic ? JoinRule.Restricted : undefined,
suggested: true,
}); });
})); }));
onFinished(filteredRoomNames.length > 0); onFinished(filteredRoomNames.length > 0);

View file

@ -136,6 +136,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
<MessageComposer <MessageComposer
room={this.props.room} room={this.props.room}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
replyInThread={true}
replyToEvent={this.state?.thread?.replyToEvent} replyToEvent={this.state?.thread?.replyToEvent}
showReplyPreview={false} showReplyPreview={false}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}

View file

@ -19,6 +19,7 @@ import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { UNSTABLE_ELEMENT_REPLY_IN_THREAD } from "matrix-js-sdk/src/@types/event";
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/Layout"; import { Layout } from "../../../settings/Layout";
@ -206,15 +207,28 @@ export default class ReplyThread extends React.Component<IProps, IState> {
return { body, html }; return { body, html };
} }
public static makeReplyMixIn(ev: MatrixEvent) { public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) {
if (!ev) return {}; if (!ev) return {};
return {
const replyMixin = {
'm.relates_to': { 'm.relates_to': {
'm.in_reply_to': { 'm.in_reply_to': {
'event_id': ev.getId(), 'event_id': ev.getId(),
}, },
}, },
}; };
/**
* @experimental
* Rendering hint for threads, only attached if true to make
* sure that Element does not start sending that property for all events
*/
if (replyInThread) {
const inReplyTo = replyMixin['m.relates_to']['m.in_reply_to'];
inReplyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = replyInThread;
}
return replyMixin;
} }
public static makeThread( public static makeThread(

View file

@ -128,7 +128,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
private onImageEnter = (e: React.MouseEvent<HTMLImageElement>): void => { private onImageEnter = (e: React.MouseEvent<HTMLImageElement>): void => {
this.setState({ hover: true }); this.setState({ hover: true });
if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifs")) {
return; return;
} }
const imgElement = e.currentTarget; const imgElement = e.currentTarget;
@ -138,7 +138,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
private onImageLeave = (e: React.MouseEvent<HTMLImageElement>): void => { private onImageLeave = (e: React.MouseEvent<HTMLImageElement>): void => {
this.setState({ hover: false }); this.setState({ hover: false });
if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifs")) {
return; return;
} }
const imgElement = e.currentTarget; const imgElement = e.currentTarget;
@ -387,7 +387,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
} }
if (this.isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) { if (this.isGif() && !SettingsStore.getValue("autoplayGifs") && !this.state.hover) {
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>; gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
} }
@ -487,7 +487,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
const contentUrl = this.getContentUrl(); const contentUrl = this.getContentUrl();
let thumbUrl; let thumbUrl;
if (this.isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) { if (this.isGif() && SettingsStore.getValue("autoplayGifs")) {
thumbUrl = contentUrl; thumbUrl = contentUrl;
} else { } else {
thumbUrl = this.getThumbUrl(); thumbUrl = this.getThumbUrl();

View file

@ -145,7 +145,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
} }
async componentDidMount() { async componentDidMount() {
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean; const autoplay = SettingsStore.getValue("autoplayVideo") as boolean;
this.loadBlurhash(); this.loadBlurhash();
if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) { if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) {
@ -209,7 +209,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
render() { render() {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos"); const autoplay = SettingsStore.getValue("autoplayVideo");
if (this.state.error !== null) { if (this.state.error !== null) {
return ( return (

View file

@ -43,11 +43,6 @@ import QuestionDialog from "../dialogs/QuestionDialog";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
function eventIsReply(mxEvent: MatrixEvent): boolean {
const relatesTo = mxEvent.getContent()["m.relates_to"];
return !!(relatesTo && relatesTo["m.in_reply_to"]);
}
function getHtmlReplyFallback(mxEvent: MatrixEvent): string { function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
const html = mxEvent.getContent().formatted_body; const html = mxEvent.getContent().formatted_body;
if (!html) { if (!html) {
@ -72,7 +67,7 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
if (isEmote) { if (isEmote) {
model = stripEmoteCommand(model); model = stripEmoteCommand(model);
} }
const isReply = eventIsReply(editedEvent); const isReply = !!editedEvent.replyEventId;
let plainPrefix = ""; let plainPrefix = "";
let htmlPrefix = ""; let htmlPrefix = "";

View file

@ -183,6 +183,7 @@ interface IProps {
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
replyInThread?: boolean;
showReplyPreview?: boolean; showReplyPreview?: boolean;
e2eStatus?: E2EStatus; e2eStatus?: E2EStatus;
compact?: boolean; compact?: boolean;
@ -204,6 +205,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private voiceRecordingButton: VoiceRecordComposerTile; private voiceRecordingButton: VoiceRecordComposerTile;
static defaultProps = { static defaultProps = {
replyInThread: false,
showReplyPreview: true, showReplyPreview: true,
compact: false, compact: false,
}; };
@ -383,6 +385,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
room={this.props.room} room={this.props.room}
placeholder={this.renderPlaceholderText()} placeholder={this.renderPlaceholderText()}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
replyInThread={this.props.replyInThread}
replyToEvent={this.props.replyToEvent} replyToEvent={this.props.replyToEvent}
onChange={this.onChange} onChange={this.onChange}
disabled={this.state.haveRecording} disabled={this.state.haveRecording}

View file

@ -57,15 +57,16 @@ import { ActionPayload } from "../../../dispatcher/payloads";
function addReplyToMessageContent( function addReplyToMessageContent(
content: IContent, content: IContent,
repliedToEvent: MatrixEvent, replyToEvent: MatrixEvent,
replyInThread: boolean,
permalinkCreator: RoomPermalinkCreator, permalinkCreator: RoomPermalinkCreator,
): void { ): void {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); const replyContent = ReplyThread.makeReplyMixIn(replyToEvent, replyInThread);
Object.assign(content, replyContent); Object.assign(content, replyContent);
// Part of Replies fallback support - prepend the text we're sending // Part of Replies fallback support - prepend the text we're sending
// with the text we're replying to // with the text we're replying to
const nestedReply = ReplyThread.getNestedReplyText(repliedToEvent, permalinkCreator); const nestedReply = ReplyThread.getNestedReplyText(replyToEvent, permalinkCreator);
if (nestedReply) { if (nestedReply) {
if (content.formatted_body) { if (content.formatted_body) {
content.formatted_body = nestedReply.html + content.formatted_body; content.formatted_body = nestedReply.html + content.formatted_body;
@ -77,8 +78,9 @@ function addReplyToMessageContent(
// exported for tests // exported for tests
export function createMessageContent( export function createMessageContent(
model: EditorModel, model: EditorModel,
permalinkCreator: RoomPermalinkCreator,
replyToEvent: MatrixEvent, replyToEvent: MatrixEvent,
replyInThread: boolean,
permalinkCreator: RoomPermalinkCreator,
): IContent { ): IContent {
const isEmote = containsEmote(model); const isEmote = containsEmote(model);
if (isEmote) { if (isEmote) {
@ -101,7 +103,7 @@ export function createMessageContent(
} }
if (replyToEvent) { if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, permalinkCreator); addReplyToMessageContent(content, replyToEvent, replyInThread, permalinkCreator);
} }
return content; return content;
@ -129,6 +131,7 @@ interface IProps {
room: Room; room: Room;
placeholder?: string; placeholder?: string;
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
replyInThread?: boolean;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
disabled?: boolean; disabled?: boolean;
onChange?(model: EditorModel): void; onChange?(model: EditorModel): void;
@ -357,7 +360,12 @@ export default class SendMessageComposer extends React.Component<IProps> {
if (cmd.category === CommandCategories.messages) { if (cmd.category === CommandCategories.messages) {
content = await this.runSlashCommand(cmd, args); content = await this.runSlashCommand(cmd, args);
if (replyToEvent) { if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator); addReplyToMessageContent(
content,
replyToEvent,
this.props.replyInThread,
this.props.permalinkCreator,
);
} }
} else { } else {
this.runSlashCommand(cmd, args); this.runSlashCommand(cmd, args);
@ -400,7 +408,12 @@ export default class SendMessageComposer extends React.Component<IProps> {
const startTime = CountlyAnalytics.getTimestamp(); const startTime = CountlyAnalytics.getTimestamp();
const { roomId } = this.props.room; const { roomId } = this.props.room;
if (!content) { if (!content) {
content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent); content = createMessageContent(
this.model,
replyToEvent,
this.props.replyInThread,
this.props.permalinkCreator,
);
} }
// don't bother sending an empty message // don't bother sending an empty message
if (!content.body.trim()) return; if (!content.body.trim()) return;

View file

@ -172,7 +172,8 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
]; ];
static IMAGES_AND_VIDEOS_SETTINGS = [ static IMAGES_AND_VIDEOS_SETTINGS = [
'urlPreviewsEnabled', 'urlPreviewsEnabled',
'autoplayGifsAndVideos', 'autoplayGifs',
'autoplayVideo',
'showImages', 'showImages',
]; ];
static TIMELINE_SETTINGS = [ static TIMELINE_SETTINGS = [

View file

@ -62,6 +62,8 @@ export interface IOpts {
roomType?: RoomType | string; roomType?: RoomType | string;
historyVisibility?: HistoryVisibility; historyVisibility?: HistoryVisibility;
parentSpace?: Room; parentSpace?: Room;
// contextually only makes sense if parentSpace is specified, if true then will be added to parentSpace as suggested
suggested?: boolean;
joinRule?: JoinRule; joinRule?: JoinRule;
} }
@ -228,7 +230,7 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
} }
}).then(() => { }).then(() => {
if (opts.parentSpace) { if (opts.parentSpace) {
return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], true); return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], opts.suggested);
} }
if (opts.associatedWithCommunity) { if (opts.associatedWithCommunity) {
return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false); return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false);

View file

@ -834,7 +834,8 @@
"Show read receipts sent by other users": "Show read receipts sent by other users", "Show read receipts sent by other users": "Show read receipts sent by other users",
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
"Always show message timestamps": "Always show message timestamps", "Always show message timestamps": "Always show message timestamps",
"Autoplay GIFs and videos": "Autoplay GIFs and videos", "Autoplay GIFs": "Autoplay GIFs",
"Autoplay videos": "Autoplay videos",
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting", "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
"Expand code blocks by default": "Expand code blocks by default", "Expand code blocks by default": "Expand code blocks by default",
"Show line numbers in code blocks": "Show line numbers in code blocks", "Show line numbers in code blocks": "Show line numbers in code blocks",

View file

@ -393,9 +393,14 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td('Always show message timestamps'), displayName: _td('Always show message timestamps'),
default: false, default: false,
}, },
"autoplayGifsAndVideos": { "autoplayGifs": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Autoplay GIFs and videos'), displayName: _td('Autoplay GIFs'),
default: false,
},
"autoplayVideo": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Autoplay videos'),
default: false, default: false,
}, },
"enableSyntaxHighlightLanguageDetection": { "enableSyntaxHighlightLanguageDetection": {

View file

@ -110,6 +110,21 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
return content ? content['enabled'] : null; return content ? content['enabled'] : null;
} }
// Special case for autoplaying videos and GIFs
if (["autoplayGifs", "autoplayVideo"].includes(settingName)) {
const settings = this.getSettings() || {};
const value = settings[settingName];
// Fallback to old combined setting
if (value === null || value === undefined) {
const oldCombinedValue = settings["autoplayGifsAndVideos"];
// Write, so that we can remove this in the future
this.setValue("autoplayGifs", roomId, oldCombinedValue);
this.setValue("autoplayVideo", roomId, oldCombinedValue);
return oldCombinedValue;
}
return value;
}
const settings = this.getSettings() || {}; const settings = this.getSettings() || {};
let preferredValue = settings[settingName]; let preferredValue = settings[settingName];

View file

@ -46,7 +46,7 @@ describe('<SendMessageComposer/>', () => {
const model = new EditorModel([], createPartCreator(), createRenderer()); const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("hello world", "insertText", { offset: 11, atNodeEnd: true }); model.update("hello world", "insertText", { offset: 11, atNodeEnd: true });
const content = createMessageContent(model, permalinkCreator); const content = createMessageContent(model, null, false, permalinkCreator);
expect(content).toEqual({ expect(content).toEqual({
body: "hello world", body: "hello world",
@ -58,7 +58,7 @@ describe('<SendMessageComposer/>', () => {
const model = new EditorModel([], createPartCreator(), createRenderer()); const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("hello *world*", "insertText", { offset: 13, atNodeEnd: true }); model.update("hello *world*", "insertText", { offset: 13, atNodeEnd: true });
const content = createMessageContent(model, permalinkCreator); const content = createMessageContent(model, null, false, permalinkCreator);
expect(content).toEqual({ expect(content).toEqual({
body: "hello *world*", body: "hello *world*",
@ -72,7 +72,7 @@ describe('<SendMessageComposer/>', () => {
const model = new EditorModel([], createPartCreator(), createRenderer()); const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("/me blinks __quickly__", "insertText", { offset: 22, atNodeEnd: true }); model.update("/me blinks __quickly__", "insertText", { offset: 22, atNodeEnd: true });
const content = createMessageContent(model, permalinkCreator); const content = createMessageContent(model, null, false, permalinkCreator);
expect(content).toEqual({ expect(content).toEqual({
body: "blinks __quickly__", body: "blinks __quickly__",
@ -86,7 +86,7 @@ describe('<SendMessageComposer/>', () => {
const model = new EditorModel([], createPartCreator(), createRenderer()); const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("//dev/null is my favourite place", "insertText", { offset: 32, atNodeEnd: true }); model.update("//dev/null is my favourite place", "insertText", { offset: 32, atNodeEnd: true });
const content = createMessageContent(model, permalinkCreator); const content = createMessageContent(model, null, false, permalinkCreator);
expect(content).toEqual({ expect(content).toEqual({
body: "/dev/null is my favourite place", body: "/dev/null is my favourite place",