feat: add Giphy support with embed component and power levels state management

This commit is contained in:
2026-04-21 21:35:47 +10:00
parent 7c824392b7
commit 9a463facce
14 changed files with 218 additions and 11 deletions

View File

@@ -182,6 +182,25 @@ export class CallService {
): Promise<void> {
const stateKey = this.config.userId;
// Check if user has permission to send this state event
const room = this.matrixClient.getRoom(roomId);
if (room) {
const powerLevelsEvent = room.currentState.getStateEvents('m.room.power_levels', '');
const powerLevelsContent = powerLevelsEvent?.getContent() || {};
const myPower = powerLevelsContent.users?.[this.config.userId] ?? powerLevelsContent.users_default ?? 0;
const requiredPower = powerLevelsContent.events?.[CALL_MEMBER_EVENT_TYPE] ?? powerLevelsContent.state_default ?? 50;
if (myPower < requiredPower) {
console.warn(
`Cannot send call member event: insufficient permissions. ` +
`User power level (${myPower}) < required power level (${requiredPower}). ` +
`Call will work locally but room members won't see your call status.`
);
return;
}
}
if (active) {
const content: CallMemberEventContent = {
'm.calls': [

View File

@@ -9,7 +9,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { ErrorCode } from '../../cs-errorcode';
import { millisecondsToMinutes } from '../../utils/common';
import { createRoomEncryptionState } from '../../components/create-room';
import { createRoomEncryptionState, createRoomPowerLevelsState } from '../../components/create-room';
import { useAlive } from '../../hooks/useAlive';
import { getDirectRoomPath } from '../../pages/pathUtils';
@@ -29,6 +29,7 @@ export function CreateChat({ defaultUserId }: CreateChatProps) {
async (userId, encrypted) => {
const initialState: ICreateRoomStateEvent[] = [];
initialState.push(createRoomPowerLevelsState());
if (encrypted) initialState.push(createRoomEncryptionState());
const result = await mx.createRoom({

View File

@@ -88,7 +88,13 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
if (portalContainer && portalContainer.children.length > 0) {
return;
}
if (shouldFocusMessageField(evt) || isKeyHotkey('mod+v', evt)) {
if (shouldFocusMessageField(evt)) {
evt.preventDefault();
ReactEditor.focus(editor);
if (evt.key.length === 1) {
editor.insertText(evt.key);
}
} else if (isKeyHotkey('mod+v', evt)) {
ReactEditor.focus(editor);
}
},

View File

@@ -31,8 +31,11 @@ import {
import { HTMLReactParserOptions } from 'html-react-parser';
import { Opts as LinkifyOpts } from 'linkifyjs';
import { ReactEditor } from 'slate-react';
import { isKeyHotkey } from 'is-hotkey';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useKeyDown } from '../../hooks/useKeyDown';
import { editableActiveElement } from '../../utils/dom';
import {
useEditor,
createMentionElement,
@@ -100,6 +103,37 @@ type ThreadViewProps = {
threadRootId: string;
};
const FN_KEYS_REGEX = /^F\d+$/;
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
const { code } = evt;
if (evt.metaKey || evt.altKey || evt.ctrlKey) {
return false;
}
if (FN_KEYS_REGEX.test(code)) return false;
if (
code.startsWith('OS') ||
code.startsWith('Meta') ||
code.startsWith('Shift') ||
code.startsWith('Alt') ||
code.startsWith('Control') ||
code.startsWith('Arrow') ||
code.startsWith('Page') ||
code.startsWith('End') ||
code.startsWith('Home') ||
code === 'Tab' ||
code === 'Space' ||
code === 'Enter' ||
code === 'NumLock' ||
code === 'ScrollLock'
) {
return false;
}
return true;
};
/**
* Renders a thread inline, replacing the main room timeline.
* Structured as direct flex siblings of `Page` to match the RoomView layout contract.
@@ -285,6 +319,29 @@ export function ThreadView({ room, threadRootId }: ThreadViewProps) {
const inputRef = useRef<HTMLDivElement>(null);
const editor = useEditor();
useKeyDown(
window,
useCallback(
(evt) => {
if (editableActiveElement()) return;
const portalContainer = document.getElementById('portalContainer');
if (portalContainer && portalContainer.children.length > 0) {
return;
}
if (shouldFocusMessageField(evt)) {
evt.preventDefault();
ReactEditor.focus(editor);
if (evt.key.length === 1) {
editor.insertText(evt.key);
}
} else if (isKeyHotkey('mod+v', evt)) {
ReactEditor.focus(editor);
}
},
[editor]
)
);
// Subscribe to thread and room timeline events.
useEffect(() => {
// eslint-disable-next-line no-console