Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Improve handling of pills in the composer #6353

Merged
merged 18 commits into from
Aug 11, 2021
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
improve types
  • Loading branch information
t3chguy committed Jul 12, 2021
commit 51f0f5718a8882897d99174a0879e610cc158223
10 changes: 5 additions & 5 deletions src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ import {
} from '../../../editor/operations';
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
import { getAutoCompleteCreator } from '../../../editor/parts';
import { getAutoCompleteCreator, Type } from '../../../editor/parts';
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
import { renderModel } from '../../../editor/render';
import TypingStore from "../../../stores/TypingStore";
@@ -157,7 +157,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
range.expandBackwardsWhile((index, offset) => {
const part = model.parts[index];
n -= 1;
return n >= 0 && (part.type === "plain" || part.type === "pill-candidate");
return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate);
});
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
if (emoticonMatch) {
@@ -548,9 +548,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const range = model.startRange(position);
range.expandBackwardsWhile((index, offset, part) => {
return part.text[offset] !== " " && part.text[offset] !== "+" && (
part.type === "plain" ||
part.type === "pill-candidate" ||
part.type === "command"
part.type === Type.Plain ||
part.type === Type.PillCandidate ||
part.type === Type.Command
);
});
const { partCreator } = model;
8 changes: 4 additions & 4 deletions src/components/views/rooms/EditMessageComposer.tsx
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ import { getCaretOffsetAndText } from '../../../editor/dom';
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize';
import { findEditableEvent } from '../../../utils/EventUtils';
import { parseEvent } from '../../../editor/deserialize';
import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts';
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import BasicMessageComposer from "./BasicMessageComposer";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@@ -242,12 +242,12 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
const parts = this.model.parts;
const firstPart = parts[0];
if (firstPart) {
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
return true;
}

if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
&& (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) {
return true;
}
}
@@ -268,7 +268,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
private getSlashCommand(): [Command, string, string] {
const commandText = this.model.parts.reduce((text, part) => {
// use mxid to textify user pills in a command
if (part.type === "user-pill") {
if (part.type === Type.UserPill) {
return text + part.resourceId;
}
return text + part.text;
6 changes: 3 additions & 3 deletions src/components/views/rooms/SendMessageComposer.tsx
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ import {
textSerialize,
unescapeMessage,
} from '../../../editor/serialize';
import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts';
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
import BasicMessageComposer from "./BasicMessageComposer";
import ReplyThread from "../elements/ReplyThread";
import { findEditableEvent } from '../../../utils/EventUtils';
@@ -240,14 +240,14 @@ export default class SendMessageComposer extends React.Component<IProps> {
const parts = this.model.parts;
const firstPart = parts[0];
if (firstPart) {
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
return true;
}
// be extra resilient when somehow the AutocompleteWrapperModel or
// CommandPartCreator fails to insert a command part, so we don't send
// a command as a message
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
&& (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) {
return true;
}
}
24 changes: 12 additions & 12 deletions src/editor/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -43,35 +43,35 @@ export default class AutocompleteWrapperModel {
) {
}

public onEscape(e: KeyboardEvent) {
public onEscape(e: KeyboardEvent): void {
this.getAutocompleterComponent().onEscape(e);
this.updateCallback({
replaceParts: [this.partCreator.plain(this.queryPart.text)],
close: true,
});
}

public close() {
public close(): void {
this.updateCallback({ close: true });
}

public hasSelection() {
public hasSelection(): boolean {
return this.getAutocompleterComponent().hasSelection();
}

public hasCompletions() {
public hasCompletions(): boolean {
const ac = this.getAutocompleterComponent();
return ac && ac.countCompletions() > 0;
}

public onEnter() {
public onEnter(): void {
this.updateCallback({ close: true });
}

/**
* If there is no current autocompletion, start one and move to the first selection.
*/
public async startSelection() {
public async startSelection(): Promise<void> {
const acComponent = this.getAutocompleterComponent();
if (acComponent.countCompletions() === 0) {
// Force completions to show for the text currently entered
@@ -81,23 +81,23 @@ export default class AutocompleteWrapperModel {
}
}

public selectPreviousSelection() {
public selectPreviousSelection(): void {
this.getAutocompleterComponent().moveSelection(-1);
}

public selectNextSelection() {
public selectNextSelection(): void {
this.getAutocompleterComponent().moveSelection(+1);
}

public onPartUpdate(part: Part, pos: DocumentPosition) {
public onPartUpdate(part: Part, pos: DocumentPosition): Promise<void> {
// cache the typed value and caret here
// so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text)
this.queryPart = part;
this.partIndex = pos.index;
return this.updateQuery(part.text);
}

public onComponentSelectionChange(completion: ICompletion) {
public onComponentSelectionChange(completion: ICompletion): void {
if (!completion) {
this.updateCallback({
replaceParts: [this.queryPart],
@@ -109,14 +109,14 @@ export default class AutocompleteWrapperModel {
}
}

public onComponentConfirm(completion: ICompletion) {
public onComponentConfirm(completion: ICompletion): void {
this.updateCallback({
replaceParts: this.partForCompletion(completion),
close: true,
});
}

private partForCompletion(completion: ICompletion) {
private partForCompletion(completion: ICompletion): Part[] {
const { completionId } = completion;
const text = completion.completion;
switch (completion.type) {
6 changes: 3 additions & 3 deletions src/editor/caret.ts
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ import { needsCaretNodeBefore, needsCaretNodeAfter } from "./render";
import Range from "./range";
import EditorModel from "./model";
import DocumentPosition, { IPosition } from "./position";
import { Part } from "./parts";
import { Part, Type } from "./parts";

export type Caret = Range | DocumentPosition;

@@ -113,7 +113,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) {
// to find newline parts
for (let i = 0; i <= partIndex; ++i) {
const part = parts[i];
if (part.type === "newline") {
if (part.type === Type.Newline) {
lineIndex += 1;
nodeIndex = -1;
prevPart = null;
@@ -128,7 +128,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) {
// and not an adjacent caret node
if (i < partIndex) {
const nextPart = parts[i + 1];
const isLastOfLine = !nextPart || nextPart.type === "newline";
const isLastOfLine = !nextPart || nextPart.type === Type.Newline;
if (needsCaretNodeAfter(part, isLastOfLine)) {
nodeIndex += 1;
}
4 changes: 2 additions & 2 deletions src/editor/deserialize.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { walkDOMDepthFirst } from "./dom";
import { checkBlockNode } from "../HtmlUtils";
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
import { PartCreator } from "./parts";
import { PartCreator, Type } from "./parts";
import SdkConfig from "../SdkConfig";

function parseAtRoomMentions(text: string, partCreator: PartCreator) {
@@ -200,7 +200,7 @@ function prefixQuoteLines(isFirstNode, parts, partCreator) {
parts.splice(0, 0, partCreator.plain(QUOTE_LINE_PREFIX));
}
for (let i = 0; i < parts.length; i += 1) {
if (parts[i].type === "newline") {
if (parts[i].type === Type.Newline) {
parts.splice(i + 1, 0, partCreator.plain(QUOTE_LINE_PREFIX));
i += 1;
}
2 changes: 1 addition & 1 deletion src/editor/diff.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ export interface IDiff {
at?: number;
}

function firstDiff(a: string, b: string) {
function firstDiff(a: string, b: string): number {
const compareLen = Math.min(a.length, b.length);
for (let i = 0; i < compareLen; ++i) {
if (a[i] !== b[i]) {
14 changes: 7 additions & 7 deletions src/editor/history.ts
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ export default class HistoryManager {
private addedSinceLastPush = false;
private removedSinceLastPush = false;

clear() {
public clear(): void {
this.stack = [];
this.newlyTypedCharCount = 0;
this.currentIndex = -1;
@@ -103,7 +103,7 @@ export default class HistoryManager {
}

// needs to persist parts and caret position
tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff) {
public tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff): boolean {
// ignore state restoration echos.
// these respect the inputType values of the input event,
// but are actually passed in from MessageEditor calling model.reset()
@@ -121,22 +121,22 @@ export default class HistoryManager {
return shouldPush;
}

ensureLastChangesPushed(model: EditorModel) {
public ensureLastChangesPushed(model: EditorModel): void {
if (this.changedSinceLastPush) {
this.pushState(model, this.lastCaret);
}
}

canUndo() {
public canUndo(): boolean {
return this.currentIndex >= 1 || this.changedSinceLastPush;
}

canRedo() {
public canRedo(): boolean {
return this.currentIndex < (this.stack.length - 1);
}

// returns state that should be applied to model
undo(model: EditorModel) {
public undo(model: EditorModel): IHistory {
if (this.canUndo()) {
this.ensureLastChangesPushed(model);
this.currentIndex -= 1;
@@ -145,7 +145,7 @@ export default class HistoryManager {
}

// returns state that should be applied to model
redo() {
public redo(): IHistory {
if (this.canRedo()) {
this.changedSinceLastPush = false;
this.currentIndex += 1;
5 changes: 3 additions & 2 deletions src/editor/offset.ts
Original file line number Diff line number Diff line change
@@ -15,16 +15,17 @@ limitations under the License.
*/

import EditorModel from "./model";
import DocumentPosition from "./position";

export default class DocumentOffset {
constructor(public offset: number, public readonly atNodeEnd: boolean) {
}

asPosition(model: EditorModel) {
public asPosition(model: EditorModel): DocumentPosition {
return model.positionForOffset(this.offset, this.atNodeEnd);
}

add(delta: number, atNodeEnd = false) {
public add(delta: number, atNodeEnd = false): DocumentOffset {
return new DocumentOffset(this.offset + delta, atNodeEnd);
}
}
26 changes: 13 additions & 13 deletions src/editor/operations.ts
Original file line number Diff line number Diff line change
@@ -15,13 +15,13 @@ limitations under the License.
*/

import Range from "./range";
import { Part } from "./parts";
import { Part, Type } from "./parts";

/**
* Some common queries and transformations on the editor model
*/

export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) {
export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]): void {
const { model } = range;
model.transform(() => {
const oldLen = range.length;
@@ -32,7 +32,7 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) {
});
}

export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) {
export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]): void {
const { model } = range;
model.transform(() => {
const oldLen = range.length;
@@ -43,29 +43,29 @@ export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) {
});
}

export function rangeStartsAtBeginningOfLine(range: Range) {
export function rangeStartsAtBeginningOfLine(range: Range): boolean {
const { model } = range;
const startsWithPartial = range.start.offset !== 0;
const isFirstPart = range.start.index === 0;
const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline";
const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === Type.Newline;
return !startsWithPartial && (isFirstPart || previousIsNewline);
}

export function rangeEndsAtEndOfLine(range: Range) {
export function rangeEndsAtEndOfLine(range: Range): boolean {
const { model } = range;
const lastPart = model.parts[range.end.index];
const endsWithPartial = range.end.offset !== lastPart.text.length;
const isLastPart = range.end.index === model.parts.length - 1;
const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === "newline";
const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === Type.Newline;
return !endsWithPartial && (isLastPart || nextIsNewline);
}

export function formatRangeAsQuote(range: Range) {
export function formatRangeAsQuote(range: Range): void {
const { model, parts } = range;
const { partCreator } = model;
for (let i = 0; i < parts.length; ++i) {
const part = parts[i];
if (part.type === "newline") {
if (part.type === Type.Newline) {
parts.splice(i + 1, 0, partCreator.plain("> "));
}
}
@@ -81,10 +81,10 @@ export function formatRangeAsQuote(range: Range) {
replaceRangeAndExpandSelection(range, parts);
}

export function formatRangeAsCode(range: Range) {
export function formatRangeAsCode(range: Range): void {
const { model, parts } = range;
const { partCreator } = model;
const needsBlock = parts.some(p => p.type === "newline");
const needsBlock = parts.some(p => p.type === Type.Newline);
if (needsBlock) {
parts.unshift(partCreator.plain("```"), partCreator.newline());
if (!rangeStartsAtBeginningOfLine(range)) {
@@ -105,9 +105,9 @@ export function formatRangeAsCode(range: Range) {

// parts helper methods
const isBlank = part => !part.text || !/\S/.test(part.text);
const isNL = part => part.type === "newline";
const isNL = part => part.type === Type.Newline;

export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix) {
export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix): void {
const { model, parts } = range;
const { partCreator } = model;

Loading