widgets (WIP, DO NOT USE!)

add back button for rooms
This commit is contained in:
OfficialDakari 2024-08-19 12:42:18 +00:00
parent a5773c3207
commit e5e35acc1c
16 changed files with 11208 additions and 10878 deletions

View file

@ -1,30 +1,4 @@
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 15
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
updates: []

19873
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -78,6 +78,7 @@
"react-quill": "2.0.0",
"react-quilljs": "2.0.2",
"react-range": "1.8.14",
"react-resizable": "3.0.5",
"react-router-dom": "6.20.0",
"sanitize-html": "2.12.1",
"showdown": "2.1.0",
@ -121,4 +122,4 @@
"vite-plugin-static-copy": "1.0.4",
"vite-plugin-top-level-await": "1.4.1"
}
}
}

View file

@ -34,7 +34,7 @@ export const toMatrixCustomHTML = (
content: string,
getDisplayName: any
): string => {
const table = {};
const table: Record<string, string> = {};
var str = parseBlockMD(
content.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
@ -45,7 +45,7 @@ export const toMatrixCustomHTML = (
})
.replaceAll(userMentionRegexp, (match: string, mxId: string) => {
const id = `{[${v4()}]}`;
table[id] = `<a href="https://matrix.to/#/${mxId}">@${getDisplayName(mxId)}</a>`;
table[id] = `<a href="https://matrix.to/#/${mxId}">${getDisplayName(mxId)}</a>`;
return id;
})
.replaceAll(roomMentionRegexp, (match: string, name: string, id: string) => {
@ -83,7 +83,6 @@ export const getMentions = (content: string): Mentions => {
user_ids.push(match[1]);
}
}
// TODO Implement @room checking
const room = /{@room}/g.test(content);
return {
user_ids, room

View file

@ -0,0 +1,8 @@
import { style } from "@vanilla-extract/css";
export const DraggableContainer = style({
position: 'absolute',
zIndex: '100',
minHeight: '500px',
//minWidth: '600px'
});

View file

@ -0,0 +1,61 @@
import { Box, config, Header, IconButton, Modal, Text } from 'folds';
import React, { useEffect, useState } from 'react';
import Draggable from 'react-draggable';
import * as css from './Modal.css';
import { ModalsType } from '../../hooks/useModals';
import Icon from '@mdi/react';
import { mdiClose } from '@mdi/js';
type ModalsProps = {
modals: ModalsType;
};
export function Modals({ modals }: ModalsProps) {
const [record, setRecord] = useState(modals.record);
useEffect(() => {
console.debug('UPDATE !!! ');
setRecord(modals.record);
}, [modals.record]);
return (
<>
{record && Object.entries(record).map(
([id, content]) => (
<Draggable
defaultPosition={{ x: 0, y: 0 }}
handle='.modal-header'
>
<div className={css.DraggableContainer}>
<Modal variant="Surface" size="500">
<Header
className='modal-header'
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">{content.title ?? 'Modal'}</Text>
</Box>
{
content.allowClose && (
<IconButton size="300" onClick={() => modals.removeModal(id)} radii="300">
<Icon size={1} path={mdiClose} />
</IconButton>
)
}
</Header>
{content.node}
</Modal>
</div>
</Draggable>
)
)}
</>
);
}

View file

@ -0,0 +1,13 @@
import { style } from "@vanilla-extract/css";
import { color, config } from "folds";
export const WidgetItem = style({
width: 'auto',
margin: '10px',
color: color.SurfaceVariant.OnContainer,
padding: config.space.S400,
justifyContent: 'space-between',
backgroundColor: color.SurfaceVariant.Container,
boxShadow: `inset 0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.ContainerLine}`,
borderRadius: config.radii.R400
});

View file

@ -0,0 +1,31 @@
import React from 'react';
import { Box, Button, Text } from "folds";
import { getText } from '../../../lang';
import * as css from './WidgetItem.css';
type WidgetItemProps = {
name?: string;
url: string;
type: string;
onClick?: () => void;
};
export function WidgetItem({ name, type, url, onClick }: WidgetItemProps) {
return (
<Box className={css.WidgetItem} direction='Row'>
<Box direction='Column'>
<Text priority='400' size='H3'>
{name ?? 'Widget'}
</Text>
<Text priority='400' size='B400'>
{type}
</Text>
</Box>
<Button variant='Primary' onClick={onClick}>
{getText('btn.widget.open')}
</Button>
</Box>
);
}

View file

@ -303,7 +303,16 @@ export function RoomNavItem({
<UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
</UnreadBadgeCenter>
)}
{muted && !optionsVisible && <Icon size={1} path={mdiBellCancel} />}
{muted && !optionsVisible && (
<IconButton
variant="Background"
fill="None"
size="300"
radii="300"
>
<Icon size={0.8} path={mdiBellCancel} />
</IconButton>
)}
</Box>
</NavItemContent>
</NavLink>

View file

@ -5,11 +5,11 @@ export const RoomCallBox = style({
width: 'auto',
//minHeight: '200px',
margin: '10px',
//backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
padding: config.space.S400,
//backgroundColor: color.SurfaceVariant.Container,
//boxShadow: `inset 0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.ContainerLine}`,
//borderRadius: config.radii.R400,
padding: config.space.S400,
gap: '20px'
});

View file

@ -1,4 +1,4 @@
import React, { FormEventHandler, MouseEventHandler, forwardRef, useEffect, useMemo, useState } from 'react';
import React, { FormEventHandler, MouseEventHandler, ReactNode, forwardRef, useEffect, useMemo, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Box,
@ -71,8 +71,10 @@ import { getReactCustomHtmlParser } from '../../plugins/react-custom-html-parser
import { HTMLReactParserOptions } from 'html-react-parser';
import { Message } from './message';
import { Image } from '../../components/media';
import { mdiAccount, mdiAccountPlus, mdiArrowLeft, mdiCheckAll, mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiDotsVertical, mdiLinkVariant, mdiMagnify, mdiPhone, mdiPin } from '@mdi/js';
import { mdiAccount, mdiAccountPlus, mdiArrowLeft, mdiCheckAll, mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiDotsVertical, mdiLinkVariant, mdiMagnify, mdiPhone, mdiPin, mdiWidgets } from '@mdi/js';
import Icon from '@mdi/react';
import { WidgetItem } from '../../components/widget/WidgetItem';
import { useModals } from '../../hooks/useModals';
type RoomMenuProps = {
room: Room;
@ -215,7 +217,9 @@ export function RoomViewHeader({
const topic = useRoomTopic(room);
const [statusMessage, setStatusMessage] = useState('');
const [showPinned, setShowPinned] = useState(false);
const [pinned, setPinned]: [any[], any] = useState([]);
const [showWidgets, setShowWidgets] = useState(false);
const [pinned, setPinned] = useState<ReactNode[]>([]);
const [widgets, setWidgets] = useState<ReactNode[]>([]);
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined;
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
@ -282,9 +286,64 @@ export function RoomViewHeader({
const [jumpAnchor, setJumpAnchor] = useState<RectCords>();
const [pageNo, setPageNo] = useState(1);
const [loadingPinList, setLoadingPinList] = useState(true);
const modals = useModals();
// officialdakari 24.07.2024 - надо зарефакторить это всё, но мне пока лень
const handleWidgetsClick = async () => {
const userId = mx.getUserId();
if (typeof userId !== 'string') return;
const profile = mx.getUser(userId);
const timeline = room.getLiveTimeline();
const state = timeline.getState(EventTimeline.FORWARDS);
const widgets = [
...(state?.getStateEvents('m.widget') ?? []),
...(state?.getStateEvents('im.vector.modular.widgets') ?? [])
];
const widgetList: ReactNode[] = [];
console.log(widgets);
if (!widgets || widgets.length < 1) {
setWidgets(
[
<Text>{getText('widgets.none')}</Text>
]
);
} else {
for (const ev of widgets) {
const content = ev.getContent();
if (typeof content.url !== 'string') continue;
const data = {
matrix_user_id: userId,
matrix_room_id: room.roomId,
matrix_display_name: profile?.displayName ?? userId,
matrix_avatar_url: profile?.avatarUrl && mx.mxcUrlToHttp(profile?.avatarUrl),
...content.data
};
var url = `${content.url}`; // Should not be a reference
for (const key in data) {
if (typeof data[key] === 'string') {
url = url.replaceAll(`$${key}`, data[key]);
}
}
if (!url.startsWith('https://')) continue;
const openWidget = () => {
modals.addModal({
allowClose: true,
title: content.name ?? 'Widget',
node: (
<iframe style={{ border: 'none' }} allow="autoplay; camera; clipboard-write; compute-pressure; display-capture; hid; microphone; screen-wake-lock" allowFullScreen src={url}></iframe>
)
});
};
widgetList.push(
<WidgetItem onClick={openWidget} name={typeof content.name === 'string' ? content.name : undefined} url={url} type={content.type} />
);
}
}
setWidgets(widgetList);
setShowWidgets(true);
};
const updatePinnedList = async () => {
const pinnedMessages = [];
const timeline = room.getLiveTimeline();
@ -384,6 +443,10 @@ export function RoomViewHeader({
setShowPinned(false);
};
const handleWidgetsClose = () => {
setShowWidgets(false);
};
const getPresenceFn = usePresences();
const handlePrevPage = () => {
@ -533,7 +596,50 @@ export function RoomViewHeader({
</FocusTrap>
</OverlayCenter>
</Overlay>
<Overlay open={showWidgets} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: handleWidgetsClose,
clickOutsideDeactivates: true
}}
>
<Modal variant="Surface" size="500">
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">{getText('widgets.title')}</Text>
</Box>
<IconButton size="300" onClick={handleWidgetsClose} radii="300">
<Icon size={1} path={mdiClose} />
</IconButton>
</Header>
<Box tabIndex={-1} direction='Column' style={{ width: 'auto', height: 'inherit' }}>
{widgets}
</Box>
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
<Box grow="Yes" gap="300">
<Box shrink="No">
<IconButton
variant="Background"
fill="None"
size="300"
radii="300"
onClick={() => history.back()}
>
<Icon size={1} path={mdiArrowLeft} />
</IconButton>
</Box>
<Box grow="Yes" alignItems="Center" gap="300">
<Avatar onClick={handleAvClick} size="300">
<RoomAvatar
@ -605,6 +711,21 @@ export function RoomViewHeader({
)}
</TooltipProvider>
)}
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
<Text>{getText('tooltip.widgets')}</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton ref={triggerRef} onClick={handleWidgetsClick}>
<Icon size={1} path={mdiWidgets} />
</IconButton>
)}
</TooltipProvider>
<TooltipProvider
position="Bottom"
offset={4}
@ -616,7 +737,7 @@ export function RoomViewHeader({
>
{(triggerRef) => (
<IconButton ref={triggerRef} onClick={handlePinnedClick}>
<Icon size={1} path={mdiPin} />
<Icon size={1} path={mdiPin} />
</IconButton>
)}
</TooltipProvider>

View file

@ -1,5 +1,4 @@
import { createContext, useContext } from 'react';
import { MatrixClient } from 'matrix-js-sdk';
const CallContext = createContext<any | null>(null);

View file

@ -0,0 +1,53 @@
import { createContext, ReactNode, useContext, useState } from 'react';
import { v4 } from 'uuid';
export type ModalsType = {
addModal: (element: Modal) => string;
removeModal: (id: string) => void;
getModals: () => Record<string, Modal> | undefined;
getModal: (id: string) => Modal | undefined;
record: Record<string, Modal> | undefined;
};
type Modal = {
node: ReactNode;
allowClose?: boolean;
title?: string;
};
const ModalsContext = createContext<ModalsType | undefined>(undefined);
export const ModalsProvider = ModalsContext.Provider;
export function useModals(): ModalsType {
const modals = useContext(ModalsContext);
if (!modals) throw new Error('Modals not initialized!');
return modals;
}
export function createModals(): ModalsType {
const [record, setRecord] = useState<Record<string, Modal>>({});
const modals: ModalsType = {
addModal: (element) => {
const id = v4();
setRecord(o => {
const updatedRecord = { ...o, [id]: element };
return updatedRecord;
});
return id;
},
getModal: (id: string): Modal | undefined => record[id] ?? undefined,
getModals: (): Record<string, Modal> | undefined => record,
removeModal: (id: string) => {
setRecord(o => {
const updatedRecord = { ...o };
delete updatedRecord[id];
return updatedRecord;
});
},
record
};
return modals;
}

View file

@ -1,5 +1,5 @@
import { Box, config, Header, Modal, Spinner, Text } from 'folds';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { Box, config, Header, IconButton, Modal, Spinner, Text } from 'folds';
import React, { ReactElement, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import initMatrix from '../../../client/initMatrix';
import { initHotkeys } from '../../../client/event/hotkeys';
import { getSecret } from '../../../client/state/auth';
@ -19,6 +19,10 @@ import Draggable from 'react-draggable';
import * as css from './ClientRoot.css';
import { CallProvider } from '../../hooks/useCall';
import { createModals, ModalsProvider } from '../../hooks/useModals';
import Icon from '@mdi/react';
import { mdiClose } from '@mdi/js';
import { Modals } from '../../components/modal/Modal';
function SystemEmojiFeature() {
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
@ -63,6 +67,40 @@ export function ClientRoot({ children }: ClientRootProps) {
}, []);
const callWindowState = useState<any>(null);
const modals = createModals();
// todo refactor that shit
// TODO means I will never do that
useEffect(() => {
const onMessage = (evt: MessageEvent<any>) => {
const { data, source } = evt;
const respond = (response: any) => {
source?.postMessage({
...data,
response
});
};
if (data.api === 'fromWidget') {
if (data.action === 'supported_api_versions') {
respond({
supported_versions: ['0.0.1', '0.0.2']
});
} else if (data.action === 'content_loaded') {
respond({ success: true });
} else if (data.action === 'send_event') {
// TODO send_event
// 1. we should know from which room that widget is
// 2. we should ask user for permission and remember permission
// 3. we should make a setting tab to manage widget permissions
}
}
};
window.addEventListener('message', onMessage);
return () => {
window.removeEventListener('message', onMessage);
};
}, []);
return (
<SpecVersions baseUrl={baseUrl!}>
@ -70,46 +108,50 @@ export function ClientRoot({ children }: ClientRootProps) {
<ClientRootLoading />
) : (
<CallProvider value={callWindowState}>
<MatrixClientProvider value={initMatrix.matrixClient!}>
<CapabilitiesAndMediaConfigLoader>
{(capabilities, mediaConfig) => (
<CapabilitiesProvider value={capabilities ?? {}}>
<MediaConfigProvider value={mediaConfig ?? {}}>
{callWindowState[0] && (
<Draggable
defaultPosition={{ x: 0, y: 0 }}
handle='.modal-header'
>
<div className={css.DraggableContainer}>
<Modal variant="Surface" size="500">
<Header
className='modal-header'
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">Call</Text>
</Box>
</Header>
{callWindowState[0]}
</Modal>
</div>
</Draggable>
)}
{children}
<Windows />
<Dialogs />
<ReusableContextMenu />
<SystemEmojiFeature />
</MediaConfigProvider>
</CapabilitiesProvider>
)}
</CapabilitiesAndMediaConfigLoader>
</MatrixClientProvider>
<ModalsProvider value={modals}>
<MatrixClientProvider value={initMatrix.matrixClient!}>
<CapabilitiesAndMediaConfigLoader>
{(capabilities, mediaConfig) => (
<CapabilitiesProvider value={capabilities ?? {}}>
<MediaConfigProvider value={mediaConfig ?? {}}>
<Modals modals={modals} />
{callWindowState[0] && (
<Draggable
defaultPosition={{ x: 0, y: 0 }}
handle='.modal-header'
>
<div className={css.DraggableContainer}>
<Modal variant="Surface" size="500">
<Header
className='modal-header'
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">Call</Text>
</Box>
</Header>
{callWindowState[0]}
</Modal>
</div>
</Draggable>
)}
{children}
<Windows />
<Dialogs />
<ReusableContextMenu />
<SystemEmojiFeature />
</MediaConfigProvider>
</CapabilitiesProvider>
)}
</CapabilitiesAndMediaConfigLoader>
</MatrixClientProvider>
</ModalsProvider>
</CallProvider>
)}
</SpecVersions>

View file

@ -864,5 +864,8 @@
"settings.new_design_input.title": "New input design",
"settings.new_design_input.desc": "Apply new design for message composer.",
"title.incoming_call": "Incoming call from {0}",
"title.incoming_video_call": "Incoming video call from {0}"
"title.incoming_video_call": "Incoming video call from {0}",
"tooltip.widgets": "Widgets",
"widgets.title": "Widgets",
"btn.widget.open": "Open"
}

File diff suppressed because it is too large Load diff