5180 fichiers 38.5 Mo 0 téléchargements 294 vues mis à jour il y a 6 heures ui
RoomToolView.tsx 15.4 Ko Télécharger le fichier
import {
    GetConfiguration,
    GetModeratorRoomInfoMessageComposer,
    ModerateRoomMessageComposer,
    ModeratorActionMessageComposer,
    ModeratorInitData,
    ModeratorRoomInfoEvent,
    RoomModerationData
} from '@octane/renderer';
import { FC, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { localizeWithFallback, OpenUrl, SendMessageComposer, TryVisitRoom } from '../../../api';
import { showModAlert, useMessageEvent, useModWindowTrackerStore } from '../../../hooks';
import roomToolXml from '../../../assets/mod-tools/xml/roomtool_frame.xml?raw';
import { NativeWindowShell } from '../native/NativeWindowShell';
import { findNativeNode, nativeCaption, nativeNumber, NativeNode, parseNativeLayout } from '../native/NativeLayout';
import { Native0Border, Native0Button, Native0Checkbox, Native0Dropmenu, Native0Frame, Native0Input, Native0Text } from '../native/NativeWindow0';
import { NativeTextTree } from '../../../common/native-text/NativeTextTree';

const rectOf = (node: NativeNode) => ({ x: nativeNumber(node, 'x'), y: nativeNumber(node, 'y'), width: nativeNumber(node, 'width'), height: nativeNumber(node, 'height') });
const SPACING = 5;

export interface RoomToolProps {
    roomId: number;
    /** Screen position the window tracker chose (right of the start panel). */
    x: number;
    y: number;
    settings: ModeratorInitData;
    /** The room the moderator is in right now (0 outside a room). */
    currentRoomId: number;
    onOpenUserInfo: (userId: number) => void;
    onOpenChatlog: (roomId: number) => void;
    onClose: () => void;
}

// Classic v75 RoomToolCtrl (RQ): the window is built from roomtool_frame and GetModeratorRoomInfo(flat) goes out when it opens; the XML placeholders show until
// the answer for this flat arrives. The caution / message buttons act on the room alert permission and only for the room the moderator is in. The event box,
// which this hotel never fills with an event, carries the room id and a refresh link kept from the earlier room tool.
/** A one-line Volter text field: 10px line plus the 2px gutter above and below. */
const TAGS_LABEL_HEIGHT = 14;

export const RoomToolView: FC<RoomToolProps> = ({ roomId, x, y, settings, currentRoomId, onOpenUserInfo, onOpenChatlog, onClose }) => {
    const root = useMemo(() => parseNativeLayout(roomToolXml), []);
    const at = (name: string) => rectOf(findNativeNode(root, name));
    const placeholder = nativeCaption(findNativeNode(root, 'message_input'));
    const refresh = () => SendMessageComposer(new GetModeratorRoomInfoMessageComposer(roomId));
    const menuCaption = nativeCaption(findNativeNode(root, 'msgTemplatesSelect'));
    const act = findNativeNode(root, 'act_cont');
    const [data, setData] = useState<RoomModerationData>(null);
    const [nameHeight, setNameHeight] = useState(0);
    const [descHeight, setDescHeight] = useState(0);
    const [tagsHeight, setTagsHeight] = useState(0);
    const [message, setMessage] = useState(placeholder);
    const [isPlaceholder, setIsPlaceholder] = useState(true);
    const [inputActive, setInputActive] = useState(false);
    const [selectedTemplate, setSelectedTemplate] = useState(-1);
    const [kick, setKick] = useState(false);
    const [lock, setLock] = useState(false);
    const [changeName, setChangeName] = useState(false);

    const resizeWindow = useModWindowTrackerStore((state) => state.resize);
    const requestedRef = useRef(false);

    useEffect(() => {
        // one request per opened window (a development double effect run must not send it twice)
        if (requestedRef.current) return;

        requestedRef.current = true;
        SendMessageComposer(new GetModeratorRoomInfoMessageComposer(roomId));
    }, [roomId]);

    useMessageEvent<ModeratorRoomInfoEvent>(ModeratorRoomInfoEvent, (event) => {
        const parser = event.getParser();

        if (parser?.data?.flatId === roomId) setData(parser.data);
    });

    const roomCont = at('room_cont');
    const roomData = at('room_data');
    const info = at('info_cont');
    const eventCont = at('event_cont');
    const actCont = at('act_cont');
    const menu = at('msgTemplatesSelect');
    const input = at('message_input');
    const footer = at('footer_cont');
    const exists = !!data && data.room.exists;
    // A room without tags comes with one empty tag: only real tags show the tags row.
    const tags = (data?.room?.tags ?? []).filter((tag) => tag.trim().length > 0);
    const hasDesc = !!data?.room?.desc?.trim();
    // _r13a7bd05aa7cbe: name, description and tags stack with no spacing; the border is the stack plus the room_data margin twice. Without a room
    // (data.room.exists false) the room box and the spacing after the info box (event_spacing) are removed from the list.
    // The tags row is as tall as its "Tags:" label (one line) or its wrapped tags, whichever is taller.
    const stackHeight = nameHeight + (hasDesc ? descHeight : 0) + (tags.length > 0 ? Math.max(TAGS_LABEL_HEIGHT, tagsHeight) : 0);
    const roomContHeight = data ? (exists ? stackHeight + 2 * roomData.y : 0) : roomCont.height;
    const items: { key: string; height: number }[] = [
        ...(data && !exists ? [] : [{ key: 'room', height: roomContHeight }]),
        { key: 'space1', height: SPACING },
        { key: 'info', height: info.height },
        ...(data && !exists ? [] : [{ key: 'space2', height: SPACING }]),
        { key: 'event', height: eventCont.height },
        { key: 'space3', height: SPACING },
        { key: 'act', height: actCont.height },
        { key: 'space4', height: SPACING },
        { key: 'menu', height: menu.height },
        { key: 'space5', height: SPACING },
        { key: 'input', height: input.height },
        { key: 'space6', height: SPACING },
        { key: 'space7', height: SPACING },
        { key: 'footer', height: footer.height }
    ];
    const row: Record<string, number> = {};
    let cursor = 0;

    for (const item of items) {
        row[item.key] = cursor;
        cursor += item.height;
    }

    const frameHeight = cursor + 32;

    // the tracker places the windows opened from this one by its current frame
    useEffect(() => resizeWindow('roomTool', `${roomId}`, nativeNumber(root, 'width'), frameHeight), [resizeWindow, roomId, root, frameHeight]);
    // The answer for this flat, the moderator being in that very room and the room alert permission: the classic tool only wires its send buttons when the answer arrives (RQ.populate attaches
    // the click listeners) and enables them by exactly this rule (RQ._r8595b44a050a80, launcher.pretty.js:281598-281636). Until then the window shows its XML (enabled looking) buttons, which
    // do nothing; the action carries no room id, so it always lands on the room the moderator is in.
    const eligible = !!data && data.flatId === currentRoomId && !!settings?.roomAlertPermission;
    const canAct = data ? eligible : true;
    // the room the sends are for right now (0: none), refreshed with every commit so a callback captured earlier still sees the present state when it runs
    const sendFlatRef = useRef(0);

    useLayoutEffect(() => {
        sendFlatRef.current = eligible ? data.flatId : 0;
    }, [eligible, data]);

    const templates = settings.roomMessageTemplates ?? [];

    // the window is disposed by its first send: a second click before it is gone must not send again
    const sentRef = useRef(false);

    const send = (caution: boolean) => {
        const flatId = sendFlatRef.current;

        if (sentRef.current || flatId === 0) return;

        if (isPlaceholder || message === '') {
            showModAlert('You must input a message to the user');

            return;
        }

        const type = kick
            ? caution
                ? ModeratorActionMessageComposer.ACTION_KICK
                : ModeratorActionMessageComposer.ACTION_MESSAGE_AND_SOFT_KICK
            : caution
              ? ModeratorActionMessageComposer.ACTION_ALERT
              : ModeratorActionMessageComposer.ACTION_MESSAGE;

        sentRef.current = true;
        SendMessageComposer(new ModeratorActionMessageComposer(type, message, ''));

        if (lock || changeName || kick) SendMessageComposer(new ModerateRoomMessageComposer(flatId, lock ? 1 : 0, changeName ? 1 : 0, kick ? 1 : 0));

        onClose();
    };

    return (
        <NativeTextTree>
            <NativeWindowShell type="roomTool" windowKey={`${roomId}`} x={x} y={y}>
                <Native0Frame caption={nativeCaption(root)} height={frameHeight} width={nativeNumber(root, 'width')} onClose={onClose}>
                    {/* the boxes (230 wide) are wider than the client area and the window clips them at the client's right edge (the dark right border does not show) */}
                    <div style={{ position: 'absolute', left: 0, top: 0, width: nativeNumber(root, 'width') - 12, height: 2000, overflow: 'hidden' }}>
                        {(!data || exists) && (
                            <Native0Border {...roomCont} height={roomContHeight} y={row.room}>
                                {data && exists && (
                                    <div style={{ position: 'absolute', left: roomData.x, top: roomData.y, width: roomData.width, height: stackHeight }}>
                                        <Native0Text bold text={data.room.name} wrap width={220} x={0} y={0} onSize={(size) => setNameHeight(size.height)} />
                                        {hasDesc && <Native0Text color={0x808080} text={data.room.desc} wrap width={220} x={0} y={nameHeight} onSize={(size) => setDescHeight(size.height)} />}
                                        {tags.length > 0 && (
                                            <div style={{ position: 'absolute', left: 0, top: nameHeight + (hasDesc ? descHeight : 0) }}>
                                                <Native0Text bold text="Tags:" width={40} x={0} y={0} />
                                                <Native0Text bold text={tags.join(', ')} wrap width={178} x={39} y={0} onSize={(size) => setTagsHeight(size.height)} />
                                            </div>
                                        )}
                                    </div>
                                )}
                            </Native0Border>
                        )}
                        <Native0Border {...info} y={row.info}>
                            <Native0Text bold text="Room owner:" width={80} x={5} y={2} />
                            <Native0Text bold text="Users in room:" width={90} x={5} y={15} />
                            <Native0Text bold text="Owner in room:" width={90} x={5} y={28} />
                            <Native0Text bold text="Has event:" width={90} x={5} y={41} />
                            <Native0Text text={data ? data.ownerName : 'sulka'} underline width={71} x={85} y={2} onClick={data ? () => onOpenUserInfo(data.ownerId) : undefined} />
                            <Native0Text text={data ? `${data.userCount}` : '18'} width={40} x={100} y={15} />
                            <Native0Text text={data ? (data.ownerInRoom ? 'Yes' : 'No') : 'yes'} width={40} x={100} y={28} />
                            <Native0Text text="no" width={40} x={100} y={41} />
                            <Native0Button height={21} label="Enter room" width={70} x={155} y={4} onClick={() => data && TryVisitRoom(data.flatId)} />
                            <Native0Button enabled={settings.chatlogsPermission} height={21} label="Chatlog" width={70} x={155} y={26} onClick={() => data && onOpenChatlog(data.flatId)} />
                            <Native0Button
                                height={21}
                                label="Edit in HK"
                                width={70}
                                x={155}
                                y={48}
                                onClick={() => {
                                    const prefix = GetConfiguration().getValue<string>('roomadmin.url');

                                    if (data && prefix) OpenUrl(`${prefix}${data.flatId}`);
                                }}
                            />
                        </Native0Border>
                        <Native0Border {...eventCont} y={row.event}>
                            <Native0Text color={0x808080} text={localizeWithFallback('modtools.native.roomid', `Room id: ${roomId}`, ['id'], [`${roomId}`])} width={150} x={5} y={1} />
                            <Native0Text bold text={localizeWithFallback('modtools.native.refresh', 'Refresh')} underline width={60} x={170} y={1} onClick={refresh} />
                        </Native0Border>
                        <Native0Border {...actCont} y={row.act}>
                            <Native0Checkbox checked={kick} enabled={settings.roomKickPermission} x={5} y={5} onToggle={() => setKick((value) => !value)} />
                            <Native0Checkbox checked={lock} x={5} y={30} onToggle={() => setLock((value) => !value)} />
                            <Native0Checkbox checked={changeName} x={5} y={54} onToggle={() => setChangeName((value) => !value)} />
                            <Native0Text text={nativeCaption(act.children[3])} wrap width={199} x={24} y={5} />
                            <Native0Text text={nativeCaption(act.children[4])} width={199} x={24} y={31} />
                            <Native0Text text={nativeCaption(act.children[5])} wrap width={199} x={24} y={48} />
                        </Native0Border>
                        <Native0Input
                            active={inputActive}
                            height={input.height}
                            value={message}
                            width={input.width}
                            x={0}
                            y={row.input}
                            onChange={(value) => setMessage(value)}
                            onFocus={() => {
                                // _r2dd96fef5d4368: the first focus clears the placeholder text
                                if (isPlaceholder) {
                                    setMessage('');
                                    setIsPlaceholder(false);
                                }

                                setInputActive(true);
                            }}
                        />
                        <div style={{ position: 'absolute', left: 0, top: row.footer }}>
                            <Native0Button enabled={canAct} height={21} label="Send Caution" width={97} x={0} y={0} onClick={() => send(true)} />
                            <Native0Button enabled={canAct} height={21} label="Send message" width={97} x={131} y={0} onClick={() => send(false)} />
                        </div>
                        <Native0Dropmenu
                            caption={selectedTemplate >= 0 ? templates[selectedTemplate] : menuCaption}
                            height={menu.height}
                            items={templates}
                            width={menu.width}
                            x={0}
                            y={row.menu}
                            onSelect={(index) => {
                                // _rd853b7a3367fa7: choosing a template puts its text in the message field
                                setSelectedTemplate(index);
                                setMessage(templates[index]);
                                setIsPlaceholder(false);
                                setInputActive(true);
                            }}
                        />
                    </div>
                </Native0Frame>
            </NativeWindowShell>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser