mirror of
https://github.com/officialdakari/Extera.git
synced 2025-04-11 23:08:46 +02:00
feat: create polls
fix: room tombstone design fix: verification badge design update README
This commit is contained in:
parent
9bdad6bac7
commit
5d21f272e2
13 changed files with 263 additions and 41 deletions
13
README.md
13
README.md
|
@ -1,17 +1,21 @@
|
|||
# Extera
|
||||
[Discussion on Matrix](https://matrix.to/#/#extera:extera.xyz)
|
||||
[CSS Themes List](https://matrix.to/#/#themes:extera.xyz)
|
||||
[CSS Themes Source](https://git.extera.xyz/OfficialDakari/ExteraThemes)
|
||||
[Extera Website's Source code](https://git.extera.xyz/OfficialDakari/ExteraWebsite)
|
||||
|
||||
This is a fork of Cinny with some improvements and new features:
|
||||
- New commands: /lenny /tableflip /unflip
|
||||
- New commands: /lenny /tableflip /unflip
|
||||
- Profile banners (like in Discord)
|
||||
- Support for polls (Only receiving, not sending)
|
||||
- Support for captions (Both sending and receiving)
|
||||
- Full Support for polls
|
||||
- Full Support for attachment captions
|
||||
- Round avatars
|
||||
- Detailed RoomNavItem: Bigger avatar and view last message
|
||||
- Ghost mode (Disable read receipts and typing status)
|
||||
- Set presence status and status message
|
||||
- Render presence status and status message in DM and members drawer
|
||||
- Hidden chats
|
||||
- Hidden chats (Broken)
|
||||
- Hide Telegram ads in channels bridged by mautrix-telegram
|
||||
- Multiple languages support (English and Russian only atm)
|
||||
- Chat backgrounds
|
||||
- Updated bubble message layout
|
||||
|
@ -19,3 +23,4 @@ This is a fork of Cinny with some improvements and new features:
|
|||
- Toggle mentioning when replying
|
||||
- Account verification status
|
||||
- Message translation
|
||||
- Custom CSS Themes
|
|
@ -67,7 +67,7 @@ export function VerificationBadge({ userId, userName }: VerificationBadgeProps)
|
|||
marginTop: '0.2rem'
|
||||
}}
|
||||
/>
|
||||
<Typography variant='subtitle2'>
|
||||
<Typography variant='subtitle2' maxHeight='1.5em' overflow='clip' textOverflow='ellipsis'>
|
||||
{verificationBadge.label}
|
||||
</Typography>
|
||||
</div>
|
||||
|
|
|
@ -129,9 +129,9 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
|||
aria-pressed={promptLeave}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ArrowBack />
|
||||
<ArrowBack sx={{ color: 'error.main' }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<ListItemText sx={{ color: 'error.main' }}>
|
||||
{getText('room_header.leave')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
|
159
src/app/features/room/NewPollMenu.tsx
Normal file
159
src/app/features/room/NewPollMenu.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { Room } from "matrix-js-sdk";
|
||||
import React, { FormEventHandler, useCallback, useEffect, useState } from "react";
|
||||
import { useMatrixClient } from "../../hooks/useMatrixClient";
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, IconButton, List, ListItem, ListItemIcon, ListItemText, Switch, TextField, Typography, useTheme } from "@mui/material";
|
||||
import { getText } from "../../../lang";
|
||||
import SettingTile from "../../molecules/setting-tile/SettingTile";
|
||||
import { Box, Text } from "folds";
|
||||
import { Add, Delete } from "@mui/icons-material";
|
||||
import { BackButtonHandler } from "../../hooks/useBackButton";
|
||||
import { AsyncStatus, useAsyncCallback } from "../../hooks/useAsyncCallback";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
type NewPollMenuProps = {
|
||||
room: Room;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
export default function NewPollMenu({ room, open, onClose }: NewPollMenuProps) {
|
||||
const mx = useMatrixClient();
|
||||
const theme = useTheme();
|
||||
const [disclosed, setDisclosed] = useState(false);
|
||||
const [maxAnswers, setMaxAnswers] = useState(1);
|
||||
const [answers, setAnswers] = useState<string[]>([]);
|
||||
const [question, setQuestion] = useState<string>('');
|
||||
|
||||
const removeAnswer = useCallback((i: number) => {
|
||||
setAnswers((answers) => {
|
||||
const newAnswers = [...answers];
|
||||
newAnswers.splice(i, 1);
|
||||
return [...newAnswers];
|
||||
});
|
||||
}, [setAnswers]);
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { answerInput } = evt.target as HTMLFormElement & {
|
||||
searchInput: HTMLInputElement;
|
||||
};
|
||||
const v = `${answerInput.value}`;
|
||||
setAnswers((answers) => {
|
||||
return [...answers, v];
|
||||
});
|
||||
answerInput.value = '';
|
||||
};
|
||||
|
||||
const [createState, create] = useAsyncCallback(
|
||||
useCallback(async () => {
|
||||
const ans = answers.map(x => ({
|
||||
id: v4(),
|
||||
'org.matrix.msc1767.text': x,
|
||||
'm.text': x
|
||||
}));
|
||||
await mx.sendEvent(
|
||||
room.roomId,
|
||||
'org.matrix.msc3381.poll.start',
|
||||
//@ts-ignore
|
||||
{
|
||||
"org.matrix.msc3381.poll.start": {
|
||||
answers: ans,
|
||||
max_selections: maxAnswers,
|
||||
kind: disclosed ? 'org.matrix.msc3381.disclosed' : 'org.matrix.msc3381.undisclosed',
|
||||
question: {
|
||||
"m.text": question,
|
||||
"org.matrix.msc1767.text": question
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
onClose();
|
||||
}, [mx, room, maxAnswers, answers, disclosed])
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
{open && <BackButtonHandler id='new-poll-menu' callback={onClose} />}
|
||||
<DialogTitle>
|
||||
{getText('new_poll.title')}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
size='small'
|
||||
fullWidth
|
||||
placeholder={getText('new_poll.question')}
|
||||
value={question}
|
||||
onChange={(evt) => setQuestion(evt.target.value)}
|
||||
/>
|
||||
<List>
|
||||
{answers.map((x, i) => (
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
color='error'
|
||||
onClick={() => removeAnswer(i)}
|
||||
edge='end'
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemText>
|
||||
{x}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Box as='form' onSubmit={handleSubmit} style={{ marginBottom: theme.spacing(3), gap: theme.spacing(1) }}>
|
||||
<TextField
|
||||
sx={{ flexGrow: 1 }}
|
||||
name='answerInput'
|
||||
size='small'
|
||||
label={getText('new_poll.add.label')}
|
||||
autoComplete='off'
|
||||
/>
|
||||
<Button
|
||||
size='small'
|
||||
variant='contained'
|
||||
startIcon={<Add />}
|
||||
type='submit'
|
||||
>
|
||||
{getText('btn.new_poll.add')}
|
||||
</Button>
|
||||
</Box>
|
||||
<TextField
|
||||
size='small'
|
||||
fullWidth
|
||||
type='number'
|
||||
label={getText('new_poll.max_answers')}
|
||||
value={maxAnswers}
|
||||
onChange={(evt) => setMaxAnswers(parseInt(evt.target.value))}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={disclosed}
|
||||
onClick={() => setDisclosed(!disclosed)}
|
||||
/>
|
||||
}
|
||||
label={getText('new_poll.disclose.title')}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>
|
||||
{getText('btn.cancel')}
|
||||
</Button>
|
||||
<LoadingButton
|
||||
onClick={create}
|
||||
disabled={maxAnswers < 1 || maxAnswers > answers.length || answers.length === 0 || question.trim().length === 0}
|
||||
loading={createState.status === AsyncStatus.Loading}
|
||||
>
|
||||
{getText('btn.new_poll.create')}
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -113,7 +113,9 @@ import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLe
|
|||
import { useVoiceRecorder } from '../../hooks/useVoiceRecorder';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
import HideReasonSelector from '../../molecules/hide-reason-selector/HideReasonSelector';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
|
||||
import { AttachFile, Poll } from '@mui/icons-material';
|
||||
import NewPollMenu from './NewPollMenu';
|
||||
|
||||
interface RoomInputProps {
|
||||
fileDropContainerRef: RefObject<HTMLElement>;
|
||||
|
@ -156,6 +158,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
);
|
||||
const uploadBoardHandlers = useRef<UploadBoardImperativeHandlers>();
|
||||
const thread = threadId ? room.getThread(threadId) : null;
|
||||
const [attachmentMenu, setAttachmentMenu] = useState<HTMLButtonElement>();
|
||||
const [showPollMenu, setPollMenu] = useState(false);
|
||||
|
||||
const ar = useVoiceRecorder();
|
||||
|
||||
|
@ -586,6 +590,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{showPollMenu && (
|
||||
<NewPollMenu
|
||||
onClose={() => setPollMenu(false)}
|
||||
open={showPollMenu}
|
||||
room={room}
|
||||
/>
|
||||
)}
|
||||
<Overlay
|
||||
open={dropZoneVisible}
|
||||
backdrop={<OverlayBackdrop />}
|
||||
|
@ -751,11 +762,32 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
}
|
||||
before={
|
||||
!ar.isRecording && (
|
||||
<IconButton size='small'
|
||||
onClick={() => pickFile('*')}
|
||||
>
|
||||
<Icon size={1} path={mdiPlusCircleOutline} />
|
||||
</IconButton>
|
||||
<>
|
||||
<Menu anchorEl={attachmentMenu} open={!!attachmentMenu} onClose={() => setAttachmentMenu(undefined)}>
|
||||
<MenuItem onClick={() => { pickFile('*'); setAttachmentMenu(undefined); }}>
|
||||
<ListItemIcon>
|
||||
<AttachFile />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{getText('attachment_menu.file')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { setPollMenu(true); setAttachmentMenu(undefined); }}>
|
||||
<ListItemIcon>
|
||||
<Poll />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{getText('attachment_menu.poll')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={(evt) => setAttachmentMenu(attachmentMenu ? undefined : evt.currentTarget)}
|
||||
>
|
||||
<Icon size={1} path={mdiPlusCircleOutline} />
|
||||
</IconButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
after={
|
||||
|
|
|
@ -7,4 +7,8 @@ export const RoomInputPlaceholder = style({
|
|||
color: color.SurfaceVariant.OnContainer,
|
||||
boxShadow: `inset 0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderRadius: config.radii.R400,
|
||||
});
|
||||
|
||||
export const RoomInputPlaceholderND = style({
|
||||
minHeight: toRem(48)
|
||||
});
|
|
@ -4,8 +4,11 @@ import classNames from 'classnames';
|
|||
|
||||
import * as css from './RoomInputPlaceholder.css';
|
||||
|
||||
export const RoomInputPlaceholder = as<'div', ComponentProps<typeof Box>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<Box className={classNames(css.RoomInputPlaceholder, className)} {...props} ref={ref} />
|
||||
type RoomInputPlaceholderProps = {
|
||||
newDesign?: boolean;
|
||||
};
|
||||
export const RoomInputPlaceholder = as<'div', ComponentProps<typeof Box> & RoomInputPlaceholderProps>(
|
||||
({ className, newDesign, ...props }, ref) => (
|
||||
<Box className={classNames(newDesign ? css.RoomInputPlaceholderND : css.RoomInputPlaceholder, className)} {...props} ref={ref} />
|
||||
)
|
||||
);
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { Box, Button, Spinner, Text, color } from 'folds';
|
||||
import { Box, color } from 'folds';
|
||||
|
||||
import * as css from './RoomTombstone.css';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
@ -9,9 +9,12 @@ import { Membership } from '../../../types/matrix/room';
|
|||
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { getText } from '../../../lang';
|
||||
import { Alert, Button, Typography } from '@mui/material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { ErrorOutline } from '@mui/icons-material';
|
||||
|
||||
type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string };
|
||||
export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) {
|
||||
type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string; newDesign?: boolean; };
|
||||
export function RoomTombstone({ roomId, body, replacementRoomId, newDesign }: RoomTombstoneProps) {
|
||||
const mx = useMatrixClient();
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
|
||||
|
@ -32,8 +35,12 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
|
|||
};
|
||||
|
||||
return (
|
||||
<RoomInputPlaceholder alignItems="Center" gap="600" className={css.RoomTombstone}>
|
||||
<Box direction="Column" grow="Yes">
|
||||
<RoomInputPlaceholder newDesign={newDesign} alignItems="Center" gap="600" className={css.RoomTombstone}>
|
||||
<ErrorOutline />
|
||||
<Typography flexGrow={1}>
|
||||
{body || getText('room_tombstone.default_reason')}
|
||||
</Typography>
|
||||
{/* <Box direction="Column" grow="Yes">
|
||||
<Text size="T400">{body || getText('romb_tombstone.default_reason')}</Text>
|
||||
{joinState.status === AsyncStatus.Error && (
|
||||
<Text style={{ color: color.Critical.Main }} size="T200">
|
||||
|
@ -41,27 +48,19 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
|
|||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
*/}
|
||||
{replacementRoom?.getMyMembership() === Membership.Join ||
|
||||
joinState.status === AsyncStatus.Success ? (
|
||||
<Button onClick={handleOpen} size="300" variant="Success" fill="Solid" radii="300">
|
||||
<Text size="B300">{getText('btn.room_tombstone.new_room')}</Text>
|
||||
<Button onClick={handleOpen}>
|
||||
{getText('btn.room_tombstone.new_room')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
<LoadingButton
|
||||
onClick={handleJoin}
|
||||
size="300"
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
before={
|
||||
joinState.status === AsyncStatus.Loading && (
|
||||
<Spinner size="100" variant="Primary" fill="Solid" />
|
||||
)
|
||||
}
|
||||
disabled={joinState.status === AsyncStatus.Loading}
|
||||
loading={joinState.status === AsyncStatus.Loading}
|
||||
>
|
||||
<Text size="B300">{getText('btn.room_tombstone.join_room')}</Text>
|
||||
</Button>
|
||||
{getText('btn.room_tombstone.join_room')}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</RoomInputPlaceholder>
|
||||
);
|
||||
|
|
|
@ -219,6 +219,7 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string; }) {
|
|||
roomId={roomId}
|
||||
body={tombstoneEvent.getContent().body}
|
||||
replacementRoomId={tombstoneEvent.getContent().replacement_room}
|
||||
newDesign={newDesignInput}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -1318,7 +1318,7 @@ export const Message = as<'div', MessageProps>(
|
|||
|
||||
return (
|
||||
<MessageBase
|
||||
className={classNames(hasBeenSent ? css.MessageBase : sending ? css.MessageBaseSending : failed ? css.MessageBaseFailed : css.MessageBase, className)}
|
||||
className={classNames(hasBeenSent ? css.MessageBase : sending ? css.MessageBaseSending : failed ? css.MessageBaseFailed : css.MessageBase, collapse ? 'cth-collapsed' : 'cth-uncollapsed', className)}
|
||||
tabIndex={0}
|
||||
data-message-status={status}
|
||||
space={messageSpacing}
|
||||
|
|
|
@ -76,6 +76,7 @@ export function ThreadView({ room, eventId, thread }: { room: Room; eventId?: st
|
|||
roomId={roomId}
|
||||
body={tombstoneEvent.getContent().body}
|
||||
replacementRoomId={tombstoneEvent.getContent().replacement_room}
|
||||
newDesign={newDesignInput}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -888,7 +888,7 @@
|
|||
"confirm.remove_widget.question": "Are you sure you want to remove this widget? It will be removed for everyone in this room.",
|
||||
"settings.reply_fallbacks.title": "Reply fallbacks",
|
||||
"settings.reply_fallbacks.desc": "Add reply into message body. Turn on, if someone doesn't see your replies.",
|
||||
"placeholder.room_input.be_careful": "Think twice before sending - you can't delete own messages...",
|
||||
"placeholder.room_input.be_careful": "Think twice before you type...",
|
||||
"m.image": "Image",
|
||||
"m.video": "Video",
|
||||
"m.audio": "Audio",
|
||||
|
@ -1012,5 +1012,14 @@
|
|||
"header.extera_customization.themes": "Themes",
|
||||
"join_alias.label": "Room address",
|
||||
"btn.open_location_in_new": "Open in browser",
|
||||
"rename.title": "Rename"
|
||||
"rename.title": "Rename",
|
||||
"attachment_menu.file": "Add a file",
|
||||
"attachment_menu.poll": "Create a poll",
|
||||
"new_poll.title": "New poll",
|
||||
"new_poll.question": "Question",
|
||||
"new_poll.add.label": "New answer",
|
||||
"btn.new_poll.add": "Add",
|
||||
"new_poll.max_answers": "Max selections",
|
||||
"new_poll.disclose.title": "Disclose votes",
|
||||
"btn.new_poll.create": "Create"
|
||||
}
|
|
@ -873,7 +873,7 @@
|
|||
"btn.widget.remove": "Убрать",
|
||||
"settings.reply_fallbacks.title": "Ответ в содержимом сообщения",
|
||||
"settings.reply_fallbacks.desc": "Добавлять ответ в содержимое сообщения. Используйте, если кто-то не видит Ваши ответы.",
|
||||
"placeholder.room_input.be_careful": "Думайте дважды перед отправкой - Вы не можете удалять свои сообщения...",
|
||||
"placeholder.room_input.be_careful": "Вы не можете удалять свои сообщения...",
|
||||
"m.image": "Изображение",
|
||||
"m.video": "Видео",
|
||||
"m.audio": "Аудио",
|
||||
|
@ -1012,5 +1012,14 @@
|
|||
"perms.banner.desc": "Минимальный уровень прав чтобы изменить баннер комнаты/пространства.",
|
||||
"join_alias.label": "Адрес комнаты",
|
||||
"btn.open_location_in_new": "Открыть в браузере",
|
||||
"rename.title": "Переименовать"
|
||||
"rename.title": "Переименовать",
|
||||
"attachment_menu.file": "Отправить файл",
|
||||
"attachment_menu.poll": "Создать опрос",
|
||||
"new_poll.title": "Новый опрос",
|
||||
"new_poll.question": "Вопрос",
|
||||
"new_poll.add.label": "Новый ответ",
|
||||
"btn.new_poll.add": "Добавить",
|
||||
"new_poll.max_answers": "Максимум ответов",
|
||||
"new_poll.disclose.title": "Предварительные результаты",
|
||||
"btn.new_poll.create": "Создать"
|
||||
}
|
Loading…
Add table
Reference in a new issue