Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/allow_changingediting_pmp_on_messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

# Allow changing/editing PMP on messages
88 changes: 86 additions & 2 deletions src/app/components/message/modals/Options.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { RoomPinnedEventsEventContent, StateEvents } from '$types/matrix-sdk';
import type {
MatrixClient,
RoomMessageEventContent,
RoomPinnedEventsEventContent,
StateEvents,
} from '$types/matrix-sdk';
import { type Room, type MatrixEvent, type Relations, EventType } from '$types/matrix-sdk';
import {
canEditEvent,
Expand Down Expand Up @@ -48,7 +53,7 @@ import { useRoomPinnedEvents } from '$hooks/useRoomPinnedEvents';
import { EmojiBoard } from '$components/emoji-board';
import { MemoizedBody, type ReactionHandler } from '$features/room/message';
import { useRecentEmoji } from '$hooks/useRecentEmoji';
import { BookmarkIcon } from '@phosphor-icons/react';
import { BookmarkIcon, UserIcon } from '@phosphor-icons/react';
import {
computeBookmarkId,
createBookmarkItem,
Expand All @@ -61,6 +66,11 @@ import { MATRIX_SABLE_UNSTABLE_FAVORITE_GIFS } from '$unstable/prefixes';
import { useFavoriteGifs } from '$hooks/useFavoriteGifs';
import type { IImageInfo } from '$types/matrix/common';
import { getIncomingMediaMxcUrl } from '../MsgTypeRenderers';
import { TemporaryPersonaPicker } from '$features/room/persona-picker/PersonaPicker';
import { type PerMessageProfile } from '$hooks/usePerMessageProfile';
import { buildReplacementPmpContent } from '$features/room/buildReplacementContent';
import { settingsAtom } from '$state/settings';
import { useSetting } from '$state/hooks/settings';

function WrappedMessage({
isModal,
Expand Down Expand Up @@ -390,6 +400,40 @@ function OptionsEmojiBoard({
);
}

type OptionsReproxyPersonaPickerProps = {
mx: MatrixClient;
mEvent: MatrixEvent;
roomId: string;
closeMenu: () => void;
anchor: RectCords;
};
function OptionsReproxyPersonaPicker({
Comment thread
j0lol marked this conversation as resolved.
mx,
mEvent,
roomId,
closeMenu,
anchor,
}: OptionsReproxyPersonaPickerProps) {
const reproxyMessage = async (profile: PerMessageProfile | undefined) => {
const content = buildReplacementPmpContent(mEvent.getContent(), mEvent.getId()!, profile);
await mx.sendMessage(roomId, content as RoomMessageEventContent);

closeMenu();
};

return (
<>
<TemporaryPersonaPicker
mx={mx}
hideTabs={true}
onPersonaSelect={reproxyMessage}
requestClose={closeMenu}
anchor={anchor}
/>
</>
);
}

export function OptionQuickMenu({
mEvent,
room,
Expand All @@ -399,6 +443,7 @@ export function OptionQuickMenu({
relations,
onReplyClick,
onEditId,
onReproxyId,
hideReadReceipts,
showDeveloperTools,
canPinEvent,
Expand Down Expand Up @@ -505,6 +550,7 @@ export function OptionQuickMenu({
relations={relations}
onReplyClick={onReplyClick}
onEditId={onEditId}
onReproxyId={onReproxyId}
hideReadReceipts={hideReadReceipts}
showDeveloperTools={showDeveloperTools}
canPinEvent={canPinEvent}
Expand Down Expand Up @@ -546,6 +592,7 @@ export type OptionMenuProps = {
startThread?: boolean
) => void;
onEditId?: (eventId?: string) => void;
onReproxyId?: (profileId?: string) => void;
hideReadReceipts?: boolean;
showDeveloperTools?: boolean;
canPinEvent?: boolean;
Expand Down Expand Up @@ -593,6 +640,7 @@ function OptionMenu({
evtTimeline &&
getEventEdits(evtTimeline.getTimelineSet(), evtId, mEvent.getType())?.getRelations();
const isEdited = !!edits?.length;
const [showPersonaSetting] = useSetting(settingsAtom, 'showPersonaSetting');

const onTotalClose = () => {
setModal(null);
Expand All @@ -606,6 +654,20 @@ function OptionMenu({

const [emojiBoardAnchor, setEmojiBoardAnchor] = useState<RectCords>();

const [reproxyPickerAnchor, setReproxyPickerAnchor] = useState<RectCords>();

const handleOpenReproxyPicker: MouseEventHandler<HTMLButtonElement> = (evt) => {
const target = isModal
? { x: 0, y: innerHeight, width: 0, height: 0 }
: (evt.currentTarget.parentElement?.parentElement?.getBoundingClientRect() ?? {
x: 0,
y: 0,
width: 0,
height: 0,
});
setReproxyPickerAnchor(target);
};

const handleOpenEmojiBoard: MouseEventHandler<HTMLButtonElement> = (evt) => {
// THIS MAGIC NUMBER SHOULD BE FIXED WHEN SOMEONE FIGURES OUT WHY THE LACK OF IT CREATES A GAP IN THE EMOJIBOARD
const target = isModal
Expand Down Expand Up @@ -634,6 +696,15 @@ function OptionMenu({
ActualMessage={<WrappedMessage isModal={isModal} ActualMessage={ActualMessage} />}
/>
)}
{reproxyPickerAnchor !== undefined && (
<OptionsReproxyPersonaPicker
mx={mx}
roomId={room.roomId}
mEvent={mEvent}
closeMenu={onTotalClose}
anchor={reproxyPickerAnchor}
/>
)}
<FocusTrap
focusTrapOptions={{
initialFocus: false,
Expand Down Expand Up @@ -779,6 +850,19 @@ function OptionMenu({
</Text>
</MenuItem>
)}
{canEditEvent(mx, mEvent) && showPersonaSetting && (
<MenuItem
size="300"
after={menuIcon(UserIcon)}
radii="300"
data-event-id={mEvent.getId()}
onClick={handleOpenReproxyPicker}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
Change Persona
</Text>
</MenuItem>
)}
{!hideReadReceipts && (
<MessageReadReceiptItem
room={room}
Expand Down
4 changes: 2 additions & 2 deletions src/app/features/room/RoomInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ import { AudioMessageRecorder } from './AudioMessageRecorder';
import * as prefix from '$unstable/prefixes';
import { PollDialog } from './poll-modals';
import { useClientConfig } from '$hooks/useClientConfig';
import { PersonaPicker, type PersonaPickerTab } from './persona-picker/PersonaPicker.tsx';
import { PersistentPersonaPicker, type PersonaPickerTab } from './persona-picker/PersonaPicker.tsx';

const LocationDialog = lazy(() =>
import('./location-modal').then((module) => ({ default: module.LocationDialog }))
Expand Down Expand Up @@ -2194,7 +2194,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
</>
)}
{pmpPickerEnable && (
<PersonaPicker
<PersistentPersonaPicker
tab={personaPickerTab}
mx={mx}
roomId={roomId}
Expand Down
57 changes: 57 additions & 0 deletions src/app/features/room/buildReplacementContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type { IContent, IMentions } from '$types/matrix-sdk';
import { MsgType, RelationType } from '$types/matrix-sdk';
import { customHtmlEqualsPlainText } from '$components/editor';
import { sanitizeText } from '$utils/sanitize';
import type { PerMessageProfile } from '$hooks/usePerMessageProfile';
import { convertPerMessageProfileToBeeperFormat } from '$hooks/usePerMessageProfile';
import { MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME } from '$unstable/prefixes';

/**
* Unified from RoomInput and MessageEditor, which had drifted: this keeps
Expand Down Expand Up @@ -92,3 +95,57 @@ export function buildReplacementContent(

return content;
}

// Replacing only the PMP in a content. No, you can't use the above function. I tried.
export function buildReplacementPmpContent(
oldContent: IContent,
eventId: string,
newProfile: PerMessageProfile | undefined
) {
const profileBeeperFormat =
newProfile && convertPerMessageProfileToBeeperFormat(newProfile, true);

// handle fallbacks
if (oldContent[MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME]) {
const plainBody = oldContent.body;
let newPlainBody = plainBody?.replace(/^.*?: /, '');

const formattedBody = oldContent.formatted_body;
let newFormattedBody = formattedBody?.replace(
/^<strong\s+data-mx-profile-fallback[^>]*>.*?<\/strong>/,
''
);

oldContent.formatted_body = newFormattedBody;
oldContent.body = newPlainBody;
}

if (newProfile) {
const escapedName = sanitizeText(newProfile.name);
const htmlPrefix = `<strong data-mx-profile-fallback>${escapedName}: </strong>`;

if (oldContent.formatted_body) {
oldContent.formatted_body = htmlPrefix + oldContent.formatted_body;
} else {
// we don't have a formatted body, but we need one
oldContent.format = 'org.matrix.custom.html';
const escapedBody = sanitizeText(oldContent.body).replaceAll('\n', '<br/>');
oldContent.formatted_body = `${htmlPrefix}${escapedBody}`;
}

const pmpPrefix = `${newProfile.name}: `;
oldContent.body = pmpPrefix + oldContent.body;
}

const newContent = {
...oldContent,
[MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME]: profileBeeperFormat,
};
const content: IContent = {
...newContent,
'm.new_content': newContent,
'm.relates_to': { event_id: eventId, rel_type: RelationType.Replace },
};

return content;
}
3 changes: 3 additions & 0 deletions src/app/features/room/message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export type MessageProps = {
startThread?: boolean
) => void;
onEditId?: (eventId?: string) => void;
onReproxyId?: (eventId?: string) => void;
onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void;
reply?: ReactNode;
reactions?: ReactNode;
Expand Down Expand Up @@ -340,6 +341,7 @@ function MessageInternal(
onReplyClick,
onReactionToggle,
onEditId,
onReproxyId,
reply,
reactions,
hideReadReceipts,
Expand Down Expand Up @@ -980,6 +982,7 @@ function MessageInternal(
relations={relations}
onReplyClick={onReplyClick}
onEditId={onEditId}
onReproxyId={onReproxyId}
hideReadReceipts={hideReadReceipts}
showDeveloperTools={showDeveloperTools}
canPinEvent={canPinEvent}
Expand Down
Loading
Loading