mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-12 02:00:28 +03:00
Edit option (#1447)
* add func to parse html to editor input * add plain to html input function * re-construct markdown * fix missing return * fix falsy condition * fix reading href instead of src of emoji * add message editor - WIP * fix plain to editor input func * add save edit message functionality * show edited event source code * focus message input on after editing message * use del tag for strike-through instead of s * prevent autocomplete from re-opening after esc * scroll out of view msg editor in view * handle up arrow edit * handle scroll to message editor without effect * revert prev commit: effect run after editor render * ignore relation event from editable * allow data-md tag for del and em in sanitize html * prevent edit without changes * ignore previous reply when replying to msg * fix up arrow edit not working sometime
This commit is contained in:
parent
152576e85d
commit
f5bcc9b851
18 changed files with 957 additions and 108 deletions
|
|
@ -5,7 +5,11 @@ export const targetFromEvent = (evt: Event, selector: string): Element | undefin
|
|||
|
||||
export const editableActiveElement = (): boolean =>
|
||||
!!document.activeElement &&
|
||||
/^(input)|(textarea)$/.test(document.activeElement.nodeName.toLowerCase());
|
||||
(document.activeElement.nodeName.toLowerCase() === 'input' ||
|
||||
document.activeElement.nodeName.toLowerCase() === 'textbox' ||
|
||||
document.activeElement.getAttribute('contenteditable') === 'true' ||
|
||||
document.activeElement.getAttribute('role') === 'input' ||
|
||||
document.activeElement.getAttribute('role') === 'textbox');
|
||||
|
||||
export const isIntersectingScrollView = (
|
||||
scrollElement: HTMLElement,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ const StrikeRule: MDRule = {
|
|||
match: (text) => text.match(STRIKE_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, g1] = match;
|
||||
return `<s data-md="${STRIKE_MD_1}">${parse(g1)}</s>`;
|
||||
return `<del data-md="${STRIKE_MD_1}">${parse(g1)}</del>`;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,15 @@ export const isRoomId = (id: string): boolean => validMxId(id) && id.startsWith(
|
|||
|
||||
export const isRoomAlias = (id: string): boolean => validMxId(id) && id.startsWith('#');
|
||||
|
||||
export const parseMatrixToUrl = (url: string): [string | undefined, string | undefined] => {
|
||||
const href = decodeURIComponent(url);
|
||||
|
||||
const match = href.match(/^https?:\/\/matrix.to\/#\/([@!$+#]\S+:[^\\?|^\s|^\\/]+)(\?(via=\S+))?/);
|
||||
if (!match) return [undefined, undefined];
|
||||
const [, g1AsMxId, , g3AsVia] = match;
|
||||
return [g1AsMxId, g3AsVia];
|
||||
};
|
||||
|
||||
export const getRoomWithCanonicalAlias = (mx: MatrixClient, alias: string): Room | undefined =>
|
||||
mx.getRooms()?.find((room) => room.getCanonicalAlias() === alias);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,22 @@ import { IconName, IconSrc } from 'folds';
|
|||
|
||||
import {
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
IPushRule,
|
||||
IPushRules,
|
||||
JoinRule,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MsgType,
|
||||
NotificationCountType,
|
||||
RelationType,
|
||||
Room,
|
||||
} from 'matrix-js-sdk';
|
||||
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
import {
|
||||
MessageEvent,
|
||||
NotificationType,
|
||||
RoomToParents,
|
||||
RoomType,
|
||||
|
|
@ -249,6 +254,21 @@ export const getRoomAvatarUrl = (mx: MatrixClient, room: Room): string | undefin
|
|||
return room.getAvatarUrl(mx.baseUrl, 32, 32, 'crop') ?? undefined;
|
||||
};
|
||||
|
||||
export const trimReplyFromBody = (body: string): string => {
|
||||
const match = body.match(/^>\s<.+?>\s.+\n\n/);
|
||||
if (!match) return body;
|
||||
return body.slice(match[0].length);
|
||||
};
|
||||
|
||||
export const trimReplyFromFormattedBody = (formattedBody: string): string => {
|
||||
const suffix = '</mx-reply>';
|
||||
const i = formattedBody.lastIndexOf(suffix);
|
||||
if (i < 0) {
|
||||
return formattedBody;
|
||||
}
|
||||
return formattedBody.slice(i + suffix.length);
|
||||
};
|
||||
|
||||
export const parseReplyBody = (userId: string, body: string) =>
|
||||
`> <${userId}> ${body.replace(/\n/g, '\n> ')}\n\n`;
|
||||
|
||||
|
|
@ -301,3 +321,52 @@ export const getReactionContent = (eventId: string, key: string, shortcode?: str
|
|||
},
|
||||
shortcode,
|
||||
});
|
||||
|
||||
export const getEventReactions = (timelineSet: EventTimelineSet, eventId: string) =>
|
||||
timelineSet.relations.getChildEventsForEvent(
|
||||
eventId,
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction
|
||||
);
|
||||
|
||||
export const getEventEdits = (timelineSet: EventTimelineSet, eventId: string, eventType: string) =>
|
||||
timelineSet.relations.getChildEventsForEvent(eventId, RelationType.Replace, eventType);
|
||||
|
||||
export const getLatestEdit = (
|
||||
targetEvent: MatrixEvent,
|
||||
editEvents: MatrixEvent[]
|
||||
): MatrixEvent | undefined => {
|
||||
const eventByTargetSender = (rEvent: MatrixEvent) =>
|
||||
rEvent.getSender() === targetEvent.getSender();
|
||||
return editEvents.sort((m1, m2) => m2.getTs() - m1.getTs()).find(eventByTargetSender);
|
||||
};
|
||||
|
||||
export const getEditedEvent = (
|
||||
mEventId: string,
|
||||
mEvent: MatrixEvent,
|
||||
timelineSet: EventTimelineSet
|
||||
): MatrixEvent | undefined => {
|
||||
const edits = getEventEdits(timelineSet, mEventId, mEvent.getType());
|
||||
return edits && getLatestEdit(mEvent, edits.getRelations());
|
||||
};
|
||||
|
||||
export const canEditEvent = (mx: MatrixClient, mEvent: MatrixEvent) =>
|
||||
mEvent.getSender() === mx.getUserId() &&
|
||||
!mEvent.isRelation() &&
|
||||
mEvent.getType() === MessageEvent.RoomMessage &&
|
||||
(mEvent.getContent().msgtype === MsgType.Text ||
|
||||
mEvent.getContent().msgtype === MsgType.Emote ||
|
||||
mEvent.getContent().msgtype === MsgType.Notice);
|
||||
|
||||
export const getLatestEditableEvt = (
|
||||
timeline: EventTimeline,
|
||||
canEdit: (mEvent: MatrixEvent) => boolean
|
||||
): MatrixEvent | undefined => {
|
||||
const events = timeline.getEvents();
|
||||
|
||||
for (let i = events.length - 1; i >= 0; i -= 1) {
|
||||
const evt = events[i];
|
||||
if (canEdit(evt)) return evt;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,12 +56,19 @@ const permittedTagToAttributes = {
|
|||
'data-mx-maths',
|
||||
'data-mx-pill',
|
||||
'data-mx-ping',
|
||||
'data-md',
|
||||
],
|
||||
div: ['data-mx-maths'],
|
||||
a: ['name', 'target', 'href', 'rel'],
|
||||
a: ['name', 'target', 'href', 'rel', 'data-md'],
|
||||
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
|
||||
ol: ['start'],
|
||||
code: ['class'],
|
||||
code: ['class', 'data-md'],
|
||||
strong: ['data-md'],
|
||||
i: ['data-md'],
|
||||
em: ['data-md'],
|
||||
u: ['data-md'],
|
||||
s: ['data-md'],
|
||||
del: ['data-md'],
|
||||
};
|
||||
|
||||
const transformFontTag: Transformer = (tagName, attribs) => ({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue