Skip to content

Display user writing notification when editing a message #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
65 changes: 33 additions & 32 deletions packages/jupyter-chat/src/components/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
setMessages([...model.messages]);
}

function handleWritersChange(_: IChatModel, writers: IUser[]) {
setCurrentWriters(writers);
function handleWritersChange(_: IChatModel, writers: IChatModel.IWriter[]) {
setCurrentWriters(writers.map(writer => writer.user));
}

model.messagesUpdated.connect(handleChatEvents);
Expand Down Expand Up @@ -357,10 +357,10 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
const [deleted, setDeleted] = useState<boolean>(false);
const [canEdit, setCanEdit] = useState<boolean>(false);
const [canDelete, setCanDelete] = useState<boolean>(false);
const [inputModel, setInputModel] = useState<IInputModel | null>(null);

// Look if the message can be deleted or edited.
useEffect(() => {
// Init canDelete and canEdit state.
setDeleted(message.deleted ?? false);
if (model.user !== undefined && !message.deleted) {
if (model.user.username === message.sender.username) {
Expand All @@ -374,36 +374,36 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
}, [model, message]);

// Create an input model only if the message is edited.
useEffect(() => {
if (edit && canEdit) {
setInputModel(() => {
let body = message.body;
message.mentions?.forEach(user => {
body = replaceSpanToMention(body, user);
});
return new InputModel({
chatContext: model.createChatContext(),
onSend: (input: string, model?: IInputModel) =>
updateMessage(message.id, input, model),
onCancel: () => cancelEdition(),
value: body,
activeCellManager: model.activeCellManager,
selectionWatcher: model.selectionWatcher,
documentManager: model.documentManager,
config: {
sendWithShiftEnter: model.config.sendWithShiftEnter
},
attachments: message.attachments,
mentions: message.mentions
});
});
} else {
setInputModel(null);
const startEdition = (): void => {
if (!canEdit) {
return;
}
}, [edit]);
let body = message.body;
message.mentions?.forEach(user => {
body = replaceSpanToMention(body, user);
});
const inputModel = new InputModel({
chatContext: model.createChatContext(),
onSend: (input: string, model?: IInputModel) =>
updateMessage(message.id, input, model),
onCancel: () => cancelEdition(),
value: body,
activeCellManager: model.activeCellManager,
selectionWatcher: model.selectionWatcher,
documentManager: model.documentManager,
config: {
sendWithShiftEnter: model.config.sendWithShiftEnter
},
attachments: message.attachments,
mentions: message.mentions
});
model.addEditionModel(message.id, inputModel);
setEdit(true);
};

// Cancel the current edition of the message.
const cancelEdition = (): void => {
model.getEditionModel(message.id)?.dispose();
setEdit(false);
};

Expand All @@ -422,6 +422,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
updatedMessage.attachments = inputModel.attachments;
updatedMessage.mentions = inputModel.mentions;
model.updateMessage!(id, updatedMessage);
model.getEditionModel(message.id)?.dispose();
setEdit(false);
};

Expand All @@ -438,10 +439,10 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
<div ref={ref} data-index={props.index}></div>
) : (
<div ref={ref} data-index={props.index}>
{edit && canEdit && inputModel ? (
{edit && canEdit && model.getEditionModel(message.id) ? (
<ChatInput
onCancel={() => cancelEdition()}
model={inputModel}
model={model.getEditionModel(message.id)!}
chatCommandRegistry={props.chatCommandRegistry}
toolbarRegistry={props.inputToolbarRegistry}
/>
Expand All @@ -450,7 +451,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
rmRegistry={rmRegistry}
markdownStr={message.body}
model={model}
edit={canEdit ? () => setEdit(true) : undefined}
edit={canEdit ? startEdition : undefined}
delete={canDelete ? () => deleteMessage(message.id) : undefined}
rendered={props.renderedPromise}
/>
Expand Down
14 changes: 14 additions & 0 deletions packages/jupyter-chat/src/input-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ export interface IInputModel extends IDisposable {
* Clear mentions list.
*/
clearMentions(): void;

/**
* A signal emitting when disposing of the model.
*/
readonly onDisposed: ISignal<InputModel, void>;
}

/**
Expand Down Expand Up @@ -411,9 +416,17 @@ export class InputModel implements IInputModel {
if (this.isDisposed) {
return;
}
this._onDisposed.emit();
this._isDisposed = true;
}

/**
* A signal emitting when disposing of the model.
*/
get onDisposed(): ISignal<InputModel, void> {
return this._onDisposed;
}

/**
* Whether the input model is disposed.
*/
Expand All @@ -438,6 +451,7 @@ export class InputModel implements IInputModel {
private _configChanged = new Signal<IInputModel, InputModel.IConfig>(this);
private _focusInputSignal = new Signal<InputModel, void>(this);
private _attachmentsChanged = new Signal<InputModel, IAttachment[]>(this);
private _onDisposed = new Signal<InputModel, void>(this);
private _isDisposed = false;
}

Expand Down
89 changes: 83 additions & 6 deletions packages/jupyter-chat/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export interface IChatModel extends IDisposable {
/**
* A signal emitting when the messages list is updated.
*/
get configChanged(): ISignal<IChatModel, IConfig>;
readonly configChanged: ISignal<IChatModel, IConfig>;

/**
* A signal emitting when unread messages change.
Expand All @@ -97,7 +97,12 @@ export interface IChatModel extends IDisposable {
/**
* A signal emitting when the writers change.
*/
readonly writersChanged?: ISignal<IChatModel, IUser[]>;
readonly writersChanged?: ISignal<IChatModel, IChatModel.IWriter[]>;

/**
* A signal emitting when the message edition input changed change.
*/
readonly messageEditionAdded: ISignal<IChatModel, IChatModel.IMessageEdition>;

/**
* Send a message, to be defined depending on the chosen technology.
Expand Down Expand Up @@ -172,12 +177,22 @@ export interface IChatModel extends IDisposable {
/**
* Update the current writers list.
*/
updateWriters(writers: IUser[]): void;
updateWriters(writers: IChatModel.IWriter[]): void;

/**
* Create the chat context that will be passed to the input model.
*/
createChatContext(): IChatContext;

/**
* Get the input model of the edited message, given its id.
*/
getEditionModel(messageID: string): IInputModel | undefined;

/**
* Add an input model of the edited message.
*/
addEditionModel(messageID: string, inputModel: IInputModel): void;
}

/**
Expand Down Expand Up @@ -418,10 +433,17 @@ export abstract class AbstractChatModel implements IChatModel {
/**
* A signal emitting when the writers change.
*/
get writersChanged(): ISignal<IChatModel, IUser[]> {
get writersChanged(): ISignal<IChatModel, IChatModel.IWriter[]> {
return this._writersChanged;
}

/**
* A signal emitting when the message edition input changed change.
*/
get messageEditionAdded(): ISignal<IChatModel, IChatModel.IMessageEdition> {
return this._messageEditionAdded;
}

/**
* Send a message, to be defined depending on the chosen technology.
* Default to no-op.
Expand Down Expand Up @@ -546,7 +568,7 @@ export abstract class AbstractChatModel implements IChatModel {
* Update the current writers list.
* This implementation only propagate the list via a signal.
*/
updateWriters(writers: IUser[]): void {
updateWriters(writers: IChatModel.IWriter[]): void {
this._writersChanged.emit(writers);
}

Expand All @@ -555,6 +577,28 @@ export abstract class AbstractChatModel implements IChatModel {
*/
abstract createChatContext(): IChatContext;

/**
* Get the input model of the edited message, given its id.
*/
getEditionModel(messageID: string): IInputModel | undefined {
return this._messageEditions.get(messageID);
}

/**
* Add an input model of the edited message.
*/
addEditionModel(messageID: string, inputModel: IInputModel): void {
// Dispose of an hypothetic previous model for this message.
this.getEditionModel(messageID)?.dispose();

this._messageEditions.set(messageID, inputModel);
this._messageEditionAdded.emit({ id: messageID, model: inputModel });

inputModel.onDisposed.connect(() => {
this._messageEditions.delete(messageID);
});
}

/**
* Add unread messages to the list.
* @param indexes - list of new indexes.
Expand Down Expand Up @@ -617,11 +661,16 @@ export abstract class AbstractChatModel implements IChatModel {
private _selectionWatcher: ISelectionWatcher | null;
private _documentManager: IDocumentManager | null;
private _notificationId: string | null = null;
private _messageEditions = new Map<string, IInputModel>();
private _messagesUpdated = new Signal<IChatModel, void>(this);
private _configChanged = new Signal<IChatModel, IConfig>(this);
private _unreadChanged = new Signal<IChatModel, number[]>(this);
private _viewportChanged = new Signal<IChatModel, number[]>(this);
private _writersChanged = new Signal<IChatModel, IUser[]>(this);
private _writersChanged = new Signal<IChatModel, IChatModel.IWriter[]>(this);
private _messageEditionAdded = new Signal<
IChatModel,
IChatModel.IMessageEdition
>(this);
}

/**
Expand Down Expand Up @@ -662,6 +711,34 @@ export namespace IChatModel {
*/
documentManager?: IDocumentManager | null;
}

/**
* Representation of a message edition.
*/
export interface IMessageEdition {
/**
* The id of the edited message.
*/
id: string;
/**
* The model of the input editing the message.
*/
model: IInputModel;
}

/**
* Writer interface, including the message ID if the writer is editing a message.
*/
export interface IWriter {
/**
* The user currently writing.
*/
user: IUser;
/**
* The message ID (optional)
*/
messageID?: string;
}
}

/**
Expand Down
31 changes: 26 additions & 5 deletions packages/jupyterlab-chat/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class LabChatModel

this.sharedModel.awareness.on('change', this.onAwarenessChange);

this.input.valueChanged.connect(this.onInputChanged);
this.input.valueChanged.connect((_, value) => this.onInputChanged(value));
this.messageEditionAdded.connect(this.onMessageEditionAdded);
}

readonly collaborative = true;
Expand Down Expand Up @@ -261,34 +262,54 @@ export class LabChatModel
/**
* Function called by the input on key pressed.
*/
onInputChanged = (_: IInputModel, value: string): void => {
onInputChanged = (value: string, messageID?: string): void => {
if (!value || !this.config.sendTypingNotification) {
return;
}
const awareness = this.sharedModel.awareness;
if (this._timeoutWriting !== null) {
window.clearTimeout(this._timeoutWriting);
}
awareness.setLocalStateField('isWriting', true);
awareness.setLocalStateField('isWriting', messageID ?? true);
this._timeoutWriting = window.setTimeout(() => {
this._resetWritingStatus();
}, WRITING_DELAY);
};

/**
* Listen to the message edition input.
*/
onMessageEditionAdded = (
_: IChatModel,
edition: IChatModel.IMessageEdition
) => {
if (edition !== null) {
const _onInputChanged = (_: IInputModel, value: string) => {
this.onInputChanged(value, edition.id);
};

edition.model.valueChanged.connect(_onInputChanged);
}
};

/**
* Triggered when an awareness state changes.
* Used to populate the writers list.
*/
onAwarenessChange = () => {
const writers: IUser[] = [];
const writers: IChatModel.IWriter[] = [];
const states = this.sharedModel.awareness.getStates();
for (const stateID of states.keys()) {
const state = states.get(stateID);
if (!state || !state.user || state.user.username === this.user.username) {
continue;
}
if (state.isWriting) {
writers.push(state.user);
const writer: IChatModel.IWriter = {
user: state.user,
messageID: state.isWriting === true ? undefined : state.isWriting
};
writers.push(writer);
}
}
this.updateWriters(writers);
Expand Down
Loading
Loading