mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-06 07:20:29 +03:00
Merge branch 'dev' into explore-persistent-server-list
This commit is contained in:
commit
8befcaebe5
113 changed files with 1258 additions and 3689 deletions
4
.github/workflows/build-pull-request.yml
vendored
4
.github/workflows/build-pull-request.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||
run: npm run build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: preview
|
||||
path: dist
|
||||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
- name: Save pr number
|
||||
run: echo ${PR_NUMBER} > ./pr.txt
|
||||
- name: Upload pr number
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: pr
|
||||
path: ./pr.txt
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# more info: https://caddyserver.com/docs/caddyfile/patterns#single-page-apps-spas
|
||||
cinny.domain.tld {
|
||||
@nativeRouter not file {path} /
|
||||
rewrite @nativeRouter {http.matchers.file.relative}
|
||||
root * /path/to/caddy/dist
|
||||
root * /path/to/cinny/dist
|
||||
try_files {path} / index.html
|
||||
file_server
|
||||
}
|
||||
|
|
|
|||
23
package-lock.json
generated
23
package-lock.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"await-to-js": "3.0.0",
|
||||
"blurhash": "2.0.4",
|
||||
"browser-encrypt-attachment": "0.3.0",
|
||||
"chroma-js": "3.1.2",
|
||||
"classnames": "2.3.2",
|
||||
"dateformat": "5.0.3",
|
||||
"dayjs": "1.11.10",
|
||||
|
|
@ -74,6 +75,7 @@
|
|||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@rollup/plugin-wasm": "6.1.1",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/file-saver": "2.0.5",
|
||||
"@types/is-hotkey": "0.1.10",
|
||||
"@types/node": "18.11.18",
|
||||
|
|
@ -97,7 +99,7 @@
|
|||
"prettier": "2.8.1",
|
||||
"sass": "1.56.2",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "5.4.12",
|
||||
"vite": "5.4.15",
|
||||
"vite-plugin-pwa": "0.20.5",
|
||||
"vite-plugin-static-copy": "1.0.4",
|
||||
"vite-plugin-top-level-await": "1.4.4"
|
||||
|
|
@ -4573,6 +4575,13 @@
|
|||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chroma-js": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-3.1.1.tgz",
|
||||
"integrity": "sha512-SFCr4edNkZ1bGaLzGz7rgR1bRzVX4MmMxwsIa3/Bh6ose8v+hRpneoizHv0KChdjxaXyjRtaMq7sCuZSzPomQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
|
|
@ -5730,6 +5739,12 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/chroma-js": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.1.2.tgz",
|
||||
"integrity": "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==",
|
||||
"license": "(BSD-3-Clause AND Apache-2.0)"
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||
|
|
@ -11289,9 +11304,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz",
|
||||
"integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==",
|
||||
"version": "5.4.15",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",
|
||||
"integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"await-to-js": "3.0.0",
|
||||
"blurhash": "2.0.4",
|
||||
"browser-encrypt-attachment": "0.3.0",
|
||||
"chroma-js": "3.1.2",
|
||||
"classnames": "2.3.2",
|
||||
"dateformat": "5.0.3",
|
||||
"dayjs": "1.11.10",
|
||||
|
|
@ -85,6 +86,7 @@
|
|||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@rollup/plugin-wasm": "6.1.1",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/file-saver": "2.0.5",
|
||||
"@types/is-hotkey": "0.1.10",
|
||||
"@types/node": "18.11.18",
|
||||
|
|
@ -108,7 +110,7 @@
|
|||
"prettier": "2.8.1",
|
||||
"sass": "1.56.2",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "5.4.12",
|
||||
"vite": "5.4.15",
|
||||
"vite-plugin-pwa": "0.20.5",
|
||||
"vite-plugin-static-copy": "1.0.4",
|
||||
"vite-plugin-top-level-await": "1.4.4"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,17 @@ export const useRoomJoinRuleIcon = (): JoinRuleIcons =>
|
|||
}),
|
||||
[]
|
||||
);
|
||||
export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
|
||||
useMemo(
|
||||
() => ({
|
||||
[JoinRule.Invite]: Icons.SpaceLock,
|
||||
[JoinRule.Knock]: Icons.SpaceLock,
|
||||
[JoinRule.Restricted]: Icons.Space,
|
||||
[JoinRule.Public]: Icons.SpaceGlobe,
|
||||
[JoinRule.Private]: Icons.SpaceLock,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
type JoinRuleLabels = Record<JoinRule, string>;
|
||||
export const useRoomJoinRuleLabel = (): JoinRuleLabels =>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { MsgType } from 'matrix-js-sdk';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { Opts } from 'linkifyjs';
|
||||
import { config } from 'folds';
|
||||
import {
|
||||
AudioContent,
|
||||
DownloadFile,
|
||||
|
|
@ -29,7 +30,7 @@ import { ImageViewer } from './image-viewer';
|
|||
import { PdfViewer } from './Pdf-viewer';
|
||||
import { TextViewer } from './text-viewer';
|
||||
import { testMatrixTo } from '../plugins/matrix-to';
|
||||
import {IImageContent} from "../../types/matrix/common";
|
||||
import { IImageContent } from '../../types/matrix/common';
|
||||
|
||||
type RenderMessageContentProps = {
|
||||
displayName: string;
|
||||
|
|
@ -70,9 +71,10 @@ export function RenderMessageContent({
|
|||
};
|
||||
const renderCaption = () => {
|
||||
const content: IImageContent = getContent();
|
||||
if(content.filename && content.filename !== content.body) {
|
||||
if (content.filename && content.filename !== content.body) {
|
||||
return (
|
||||
<MText
|
||||
style={{ marginTop: config.space.S200 }}
|
||||
edited={edited}
|
||||
content={content}
|
||||
renderBody={(props) => (
|
||||
|
|
@ -85,10 +87,10 @@ export function RenderMessageContent({
|
|||
)}
|
||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderFile = () => (
|
||||
<>
|
||||
|
|
@ -119,7 +121,6 @@ export function RenderMessageContent({
|
|||
>
|
||||
<DownloadFile body={body} mimeType={mimeType} url={url} encInfo={encInfo} info={info} />
|
||||
</FileContent>
|
||||
|
||||
)}
|
||||
outlined={outlineAttachment}
|
||||
/>
|
||||
|
|
@ -234,7 +235,6 @@ export function RenderMessageContent({
|
|||
/>
|
||||
{renderCaption()}
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +251,6 @@ export function RenderMessageContent({
|
|||
/>
|
||||
{renderCaption()}
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import React, { CSSProperties, ReactNode } from 'react';
|
||||
import { Box, Chip, Icon, Icons, Text, toRem } from 'folds';
|
||||
import { IContent } from 'matrix-js-sdk';
|
||||
import { JUMBO_EMOJI_REG, URL_REG } from '../../utils/regex';
|
||||
|
|
@ -74,8 +74,9 @@ type MTextProps = {
|
|||
content: Record<string, unknown>;
|
||||
renderBody: (props: RenderBodyProps) => ReactNode;
|
||||
renderUrlsPreview?: (urls: string[]) => ReactNode;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
export function MText({ edited, content, renderBody, renderUrlsPreview }: MTextProps) {
|
||||
export function MText({ edited, content, renderBody, renderUrlsPreview, style }: MTextProps) {
|
||||
const { body, formatted_body: customBody } = content;
|
||||
|
||||
if (typeof body !== 'string') return <BrokenContent />;
|
||||
|
|
@ -88,6 +89,7 @@ export function MText({ edited, content, renderBody, renderUrlsPreview }: MTextP
|
|||
<MessageTextBody
|
||||
preWrap={typeof customBody !== 'string'}
|
||||
jumboEmoji={JUMBO_EMOJI_REG.test(trimmedBody)}
|
||||
style={style}
|
||||
>
|
||||
{renderBody({
|
||||
body: trimmedBody,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { Box, Icon, Icons, Text, as, color, toRem } from 'folds';
|
|||
import { EventTimelineSet, Room } from 'matrix-js-sdk';
|
||||
import React, { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getMemberDisplayName, trimReplyFromBody } from '../../utils/room';
|
||||
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||
import { LinePlaceholder } from './placeholder';
|
||||
|
|
@ -11,6 +10,8 @@ import * as css from './Reply.css';
|
|||
import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content';
|
||||
import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
|
||||
import { useRoomEvent } from '../../hooks/useRoomEvent';
|
||||
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
||||
type ReplyLayoutProps = {
|
||||
userColor?: string;
|
||||
|
|
@ -49,10 +50,28 @@ type ReplyProps = {
|
|||
replyEventId: string;
|
||||
threadRootId?: string | undefined;
|
||||
onClick?: MouseEventHandler | undefined;
|
||||
getPowerLevel?: (userId: string) => number;
|
||||
getPowerLevelTag?: GetPowerLevelTag;
|
||||
accessibleTagColors?: Map<string, string>;
|
||||
legacyUsernameColor?: boolean;
|
||||
};
|
||||
|
||||
export const Reply = as<'div', ReplyProps>(
|
||||
({ room, timelineSet, replyEventId, threadRootId, onClick, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
room,
|
||||
timelineSet,
|
||||
replyEventId,
|
||||
threadRootId,
|
||||
onClick,
|
||||
getPowerLevel,
|
||||
getPowerLevelTag,
|
||||
accessibleTagColors,
|
||||
legacyUsernameColor,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []);
|
||||
const getFromLocalTimeline = useCallback(
|
||||
() => timelineSet?.findEventById(replyEventId),
|
||||
|
|
@ -62,6 +81,11 @@ export const Reply = as<'div', ReplyProps>(
|
|||
|
||||
const { body } = replyEvent?.getContent() ?? {};
|
||||
const sender = replyEvent?.getSender();
|
||||
const senderPL = sender && getPowerLevel?.(sender);
|
||||
const powerTag = typeof senderPL === 'number' ? getPowerLevelTag?.(senderPL) : undefined;
|
||||
const tagColor = powerTag?.color ? accessibleTagColors?.get(powerTag.color) : undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(sender ?? replyEventId) : tagColor;
|
||||
|
||||
const fallbackBody = replyEvent?.isRedacted() ? (
|
||||
<MessageDeletedContent />
|
||||
|
|
@ -79,7 +103,7 @@ export const Reply = as<'div', ReplyProps>(
|
|||
)}
|
||||
<ReplyLayout
|
||||
as="button"
|
||||
userColor={sender ? colorMXID(sender) : undefined}
|
||||
userColor={usernameColor}
|
||||
username={
|
||||
sender && (
|
||||
<Text size="T300" truncate>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ export const Username = as<'span'>(({ as: AsUsername = 'span', className, ...pro
|
|||
<AsUsername className={classNames(css.Username, className)} {...props} ref={ref} />
|
||||
));
|
||||
|
||||
export const UsernameBold = as<'b'>(({ as: AsUsernameBold = 'b', className, ...props }, ref) => (
|
||||
<AsUsernameBold className={classNames(css.UsernameBold, className)} {...props} ref={ref} />
|
||||
));
|
||||
|
||||
export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice?: boolean }>(
|
||||
({ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, ...props }, ref) => (
|
||||
<Text
|
||||
|
|
|
|||
|
|
@ -157,6 +157,10 @@ export const Username = style({
|
|||
},
|
||||
});
|
||||
|
||||
export const UsernameBold = style({
|
||||
fontWeight: 550,
|
||||
});
|
||||
|
||||
export const MessageTextBody = recipe({
|
||||
base: {
|
||||
wordBreak: 'break-word',
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ type PowerColorBadgeProps = {
|
|||
export const PowerColorBadge = as<'span', PowerColorBadgeProps>(
|
||||
({ as: AsPowerColorBadge = 'span', color, className, style, ...props }, ref) => (
|
||||
<AsPowerColorBadge
|
||||
className={classNames(css.PowerColorBadge, className)}
|
||||
className={classNames(css.PowerColorBadge, { [css.PowerColorBadgeNone]: !color }, className)}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
...style,
|
||||
|
|
|
|||
|
|
@ -3,13 +3,30 @@ import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
|||
import { color, config, DefaultReset, toRem } from 'folds';
|
||||
|
||||
export const PowerColorBadge = style({
|
||||
display: 'inline-block',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
width: toRem(16),
|
||||
height: toRem(16),
|
||||
backgroundColor: color.Surface.OnContainer,
|
||||
borderRadius: config.radii.Pill,
|
||||
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||
border: `${config.borderWidth.B300} solid ${color.Secondary.ContainerLine}`,
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const PowerColorBadgeNone = style({
|
||||
selectors: {
|
||||
'&::before': {
|
||||
content: '',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
height: config.borderWidth.B300,
|
||||
backgroundColor: color.Critical.Main,
|
||||
|
||||
position: 'absolute',
|
||||
transform: `rotateZ(-45deg)`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const PowerIconSize = createVar();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { MatrixError } from 'matrix-js-sdk';
|
|||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import {
|
||||
|
|
@ -65,7 +65,7 @@ export function RoomPublishedAddresses({ powerLevels }: RoomPublishedAddressesPr
|
|||
title="Published Addresses"
|
||||
description={
|
||||
<span>
|
||||
If room access is <b>Public</b>, Published addresses will be used to join by anyone.
|
||||
If access is <b>Public</b>, Published addresses will be used to join by anyone.
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
|
@ -19,7 +19,7 @@ import React, { useCallback, useState } from 'react';
|
|||
import { MatrixError } from 'matrix-js-sdk';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
|
|
@ -16,7 +16,7 @@ import { HistoryVisibility, MatrixError } from 'matrix-js-sdk';
|
|||
import { RoomHistoryVisibilityEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
|
|
@ -7,9 +7,10 @@ import {
|
|||
JoinRulesSwitcher,
|
||||
useRoomJoinRuleIcon,
|
||||
useRoomJoinRuleLabel,
|
||||
useSpaceJoinRuleIcon,
|
||||
} from '../../../components/JoinRulesSwitcher';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
|
|
@ -60,6 +61,7 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
}, [allowRestricted, allowKnock, space]);
|
||||
|
||||
const icons = useRoomJoinRuleIcon();
|
||||
const spaceIcons = useSpaceJoinRuleIcon();
|
||||
const labels = useRoomJoinRuleLabel();
|
||||
|
||||
const [submitState, submit] = useAsyncCallback(
|
||||
|
|
@ -99,11 +101,15 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Room Access"
|
||||
description="Change how people can join the room."
|
||||
title={room.isSpaceRoom() ? 'Space Access' : 'Room Access'}
|
||||
description={
|
||||
room.isSpaceRoom()
|
||||
? 'Change how people can join the space.'
|
||||
: 'Change how people can join the room.'
|
||||
}
|
||||
after={
|
||||
<JoinRulesSwitcher
|
||||
icons={icons}
|
||||
icons={room.isSpaceRoom() ? spaceIcons : icons}
|
||||
labels={labels}
|
||||
rules={joinRules}
|
||||
value={rule}
|
||||
|
|
@ -17,7 +17,7 @@ import Linkify from 'linkify-react';
|
|||
import classNames from 'classnames';
|
||||
import { JoinRule, MatrixError } from 'matrix-js-sdk';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import {
|
||||
useRoomAvatar,
|
||||
|
|
@ -198,7 +198,12 @@ export function RoomProfileEdit({
|
|||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => (
|
||||
<RoomIcon size="400" joinRule={joinRule?.join_rule ?? JoinRule.Invite} filled />
|
||||
<RoomIcon
|
||||
space={room.isSpaceRoom()}
|
||||
size="400"
|
||||
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
|
|
@ -338,7 +343,12 @@ export function RoomProfile({ powerLevels }: RoomProfileProps) {
|
|||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => (
|
||||
<RoomIcon size="400" joinRule={joinRule?.join_rule ?? JoinRule.Invite} filled />
|
||||
<RoomIcon
|
||||
space={room.isSpaceRoom()}
|
||||
size="400"
|
||||
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { Box, color, Spinner, Switch, Text } from 'folds';
|
||||
import { MatrixError } from 'matrix-js-sdk';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { useRoomDirectoryVisibility } from '../../../hooks/useRoomDirectoryVisibility';
|
||||
|
|
@ -20,7 +20,7 @@ import FocusTrap from 'focus-trap-react';
|
|||
import { MatrixError } from 'matrix-js-sdk';
|
||||
import { RoomCreateEventContent, RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
|
|
@ -39,7 +39,7 @@ type RoomUpgradeProps = {
|
|||
export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const createContent = useStateEvent(
|
||||
room,
|
||||
StateEvent.RoomCreate
|
||||
|
|
@ -66,15 +66,23 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
|||
const handleOpenRoom = () => {
|
||||
if (replacementRoom) {
|
||||
requestClose();
|
||||
if (room.isSpaceRoom()) {
|
||||
navigateSpace(replacementRoom);
|
||||
} else {
|
||||
navigateRoom(replacementRoom);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenOldRoom = () => {
|
||||
if (predecessorRoomId) {
|
||||
requestClose();
|
||||
if (room.isSpaceRoom()) {
|
||||
navigateSpace(predecessorRoomId);
|
||||
} else {
|
||||
navigateRoom(predecessorRoomId, createContent.predecessor?.event_id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [upgradeState, upgrade] = useAsyncCallback(
|
||||
|
|
@ -110,10 +118,11 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
|||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Upgrade Room"
|
||||
title={room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}
|
||||
description={
|
||||
replacementRoom
|
||||
? tombstoneContent.body || 'This room has been replaced!'
|
||||
? tombstoneContent.body ||
|
||||
`This ${room.isSpaceRoom() ? 'space' : 'room'} has been replaced!`
|
||||
: `Current room version: ${roomVersion}.`
|
||||
}
|
||||
after={
|
||||
|
|
@ -127,7 +136,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
|||
radii="300"
|
||||
onClick={handleOpenOldRoom}
|
||||
>
|
||||
<Text size="B300">Old Room</Text>
|
||||
<Text size="B300">{room.isSpaceRoom() ? 'Old Space' : 'Old Room'}</Text>
|
||||
</Button>
|
||||
)}
|
||||
{replacementRoom ? (
|
||||
|
|
@ -138,7 +147,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
|||
radii="300"
|
||||
onClick={handleOpenRoom}
|
||||
>
|
||||
<Text size="B300">Open New Room</Text>
|
||||
<Text size="B300">{room.isSpaceRoom() ? 'Open New Space' : 'Open New Room'}</Text>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
|
@ -183,7 +192,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
|||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Room Upgrade</Text>
|
||||
<Text size="H4">{room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={() => setPrompt(false)} radii="300">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
|
@ -203,7 +212,9 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
|||
/>
|
||||
</Box>
|
||||
<Button type="submit" variant="Secondary">
|
||||
<Text size="B400">Upgrade Room</Text>
|
||||
<Text size="B400">
|
||||
{room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Dialog>
|
||||
7
src/app/features/common-settings/general/index.ts
Normal file
7
src/app/features/common-settings/general/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export * from './RoomAddress';
|
||||
export * from './RoomEncryption';
|
||||
export * from './RoomHistoryVisibility';
|
||||
export * from './RoomJoinRules';
|
||||
export * from './RoomProfile';
|
||||
export * from './RoomPublish';
|
||||
export * from './RoomUpgrade';
|
||||
|
|
@ -287,6 +287,13 @@ export function Members({ requestClose }: MembersProps) {
|
|||
<Spinner />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!fetchingMembers && !result && flattenTagMembers.length === 0 && (
|
||||
<Text style={{ padding: config.space.S300 }} align="Center">
|
||||
{`No "${membershipFilter.name}" Members`}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box
|
||||
style={{
|
||||
position: 'relative',
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
PermissionLocation,
|
||||
usePowerLevelsAPI,
|
||||
} from '../../../hooks/usePowerLevels';
|
||||
import { usePermissionGroups } from './usePermissionItems';
|
||||
import { PermissionGroup } from './types';
|
||||
import { getPowers, usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
|
|
@ -27,8 +27,9 @@ const USER_DEFAULT_LOCATION: PermissionLocation = {
|
|||
|
||||
type PermissionGroupsProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
permissionGroups: PermissionGroup[];
|
||||
};
|
||||
export function PermissionGroups({ powerLevels }: PermissionGroupsProps) {
|
||||
export function PermissionGroups({ powerLevels, permissionGroups }: PermissionGroupsProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const alive = useAlive();
|
||||
|
|
@ -40,8 +41,6 @@ export function PermissionGroups({ powerLevels }: PermissionGroupsProps) {
|
|||
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const maxPower = useMemo(() => Math.max(...getPowers(powerLevelTags)), [powerLevelTags]);
|
||||
|
||||
const permissionGroups = usePermissionGroups();
|
||||
|
||||
const [permissionUpdate, setPermissionUpdate] = useState<Map<PermissionLocation, number>>(
|
||||
new Map()
|
||||
);
|
||||
|
|
@ -24,16 +24,16 @@ import { PowerColorBadge, PowerIcon } from '../../../components/power';
|
|||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { usePermissionGroups } from './usePermissionItems';
|
||||
import { PermissionGroup } from './types';
|
||||
|
||||
type PeekPermissionsProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
power: number;
|
||||
permissionGroups: PermissionGroup[];
|
||||
children: (handleOpen: MouseEventHandler<HTMLButtonElement>, opened: boolean) => ReactNode;
|
||||
};
|
||||
function PeekPermissions({ powerLevels, power, children }: PeekPermissionsProps) {
|
||||
function PeekPermissions({ powerLevels, power, permissionGroups, children }: PeekPermissionsProps) {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const permissionGroups = usePermissionGroups();
|
||||
|
||||
const handleOpen: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
|
|
@ -101,9 +101,10 @@ function PeekPermissions({ powerLevels, power, children }: PeekPermissionsProps)
|
|||
|
||||
type PowersProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
permissionGroups: PermissionGroup[];
|
||||
onEdit?: () => void;
|
||||
};
|
||||
export function Powers({ powerLevels, onEdit }: PowersProps) {
|
||||
export function Powers({ powerLevels, permissionGroups, onEdit }: PowersProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const room = useRoom();
|
||||
|
|
@ -144,7 +145,12 @@ export function Powers({ powerLevels, onEdit }: PowersProps) {
|
|||
const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon);
|
||||
|
||||
return (
|
||||
<PeekPermissions key={power} powerLevels={powerLevels} power={power}>
|
||||
<PeekPermissions
|
||||
key={power}
|
||||
powerLevels={powerLevels}
|
||||
power={power}
|
||||
permissionGroups={permissionGroups}
|
||||
>
|
||||
{(openMenu, opened) => (
|
||||
<Chip
|
||||
onClick={openMenu}
|
||||
4
src/app/features/common-settings/permissions/index.ts
Normal file
4
src/app/features/common-settings/permissions/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from './PermissionGroups';
|
||||
export * from './Powers';
|
||||
export * from './PowersEditor';
|
||||
export * from './types';
|
||||
12
src/app/features/common-settings/permissions/types.ts
Normal file
12
src/app/features/common-settings/permissions/types.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { PermissionLocation } from '../../../hooks/usePowerLevels';
|
||||
|
||||
export type PermissionItem = {
|
||||
location: PermissionLocation;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type PermissionGroup = {
|
||||
name: string;
|
||||
items: PermissionItem[];
|
||||
};
|
||||
6
src/app/features/common-settings/styles.css.ts
Normal file
6
src/app/features/common-settings/styles.css.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { config } from 'folds';
|
||||
|
||||
export const SequenceCardStyle = style({
|
||||
padding: config.space.S300,
|
||||
});
|
||||
|
|
@ -18,7 +18,7 @@ import {
|
|||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
|
||||
import { openInviteUser, openSpaceSettings } from '../../../client/action/navigation';
|
||||
import { openInviteUser } from '../../../client/action/navigation';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||
|
|
@ -26,6 +26,7 @@ import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
|||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
|
||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
||||
|
||||
type HierarchyItemWithParent = HierarchyItem & {
|
||||
parentId: string;
|
||||
|
|
@ -153,11 +154,12 @@ function SettingsMenuItem({
|
|||
disabled?: boolean;
|
||||
}) {
|
||||
const openRoomSettings = useOpenRoomSettings();
|
||||
const openSpaceSettings = useOpenSpaceSettings();
|
||||
const space = useSpaceOptionally();
|
||||
|
||||
const handleSettings = () => {
|
||||
if ('space' in item) {
|
||||
openSpaceSettings(item.roomId);
|
||||
openSpaceSettings(item.roomId, item.parentId);
|
||||
} else {
|
||||
openRoomSettings(item.roomId, space?.roomId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|||
import { RoomAvatar } from '../../components/room-avatar';
|
||||
import { nameInitials } from '../../utils/common';
|
||||
import * as css from './LobbyHeader.css';
|
||||
import { openInviteUser, openSpaceSettings } from '../../../client/action/navigation';
|
||||
import { openInviteUser } from '../../../client/action/navigation';
|
||||
import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||
|
|
@ -35,6 +35,7 @@ import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
|||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
||||
import { mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
||||
|
||||
type LobbyMenuProps = {
|
||||
roomId: string;
|
||||
|
|
@ -46,6 +47,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
|||
const mx = useMatrixClient();
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
||||
const openSpaceSettings = useOpenSpaceSettings();
|
||||
|
||||
const handleInvite = () => {
|
||||
openInviteUser(roomId);
|
||||
|
|
@ -132,7 +134,9 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||
|
||||
const name = useRoomName(space);
|
||||
const avatarMxc = useRoomAvatar(space);
|
||||
const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
|
||||
const avatarUrl = avatarMxc
|
||||
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ export function MessageSearch({
|
|||
const allRooms = useRooms(mx, allRoomsAtom, mDirects);
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
|
@ -297,6 +299,7 @@ export function MessageSearch({
|
|||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
onOpen={navigateRoom}
|
||||
legacyUsernameColor={legacyUsernameColor || mDirects.has(groupRoom.roomId)}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
Reply,
|
||||
Time,
|
||||
Username,
|
||||
UsernameBold,
|
||||
} from '../../components/message';
|
||||
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
||||
import { Image } from '../../components/media';
|
||||
|
|
@ -32,13 +33,21 @@ import { ImageViewer } from '../../components/image-viewer';
|
|||
import * as customHtmlCss from '../../styles/CustomHtml.css';
|
||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||
import { getMemberAvatarMxc, getMemberDisplayName, getRoomAvatarUrl } from '../../utils/room';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { ResultItem } from './useMessageSearch';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { UserAvatar } from '../../components/user-avatar';
|
||||
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||
import {
|
||||
getTagIconSrc,
|
||||
useAccessibleTagColors,
|
||||
usePowerLevelTags,
|
||||
} from '../../hooks/usePowerLevelTags';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { PowerIcon } from '../../components/power';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
||||
type SearchResultGroupProps = {
|
||||
room: Room;
|
||||
|
|
@ -47,6 +56,7 @@ type SearchResultGroupProps = {
|
|||
mediaAutoLoad?: boolean;
|
||||
urlPreview?: boolean;
|
||||
onOpen: (roomId: string, eventId: string) => void;
|
||||
legacyUsernameColor?: boolean;
|
||||
};
|
||||
export function SearchResultGroup({
|
||||
room,
|
||||
|
|
@ -55,11 +65,18 @@ export function SearchResultGroup({
|
|||
mediaAutoLoad,
|
||||
urlPreview,
|
||||
onOpen,
|
||||
legacyUsernameColor,
|
||||
}: SearchResultGroupProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
|
||||
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
|
||||
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const theme = useTheme();
|
||||
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
|
||||
|
||||
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
|
||||
|
|
@ -81,7 +98,15 @@ export function SearchResultGroup({
|
|||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
}),
|
||||
[mx, room, linkifyOpts, highlightRegex, mentionClickHandler, spoilerClickHandler, useAuthentication]
|
||||
[
|
||||
mx,
|
||||
room,
|
||||
linkifyOpts,
|
||||
highlightRegex,
|
||||
mentionClickHandler,
|
||||
spoilerClickHandler,
|
||||
useAuthentication,
|
||||
]
|
||||
);
|
||||
|
||||
const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>(
|
||||
|
|
@ -197,6 +222,17 @@ export function SearchResultGroup({
|
|||
const threadRootId =
|
||||
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
|
||||
|
||||
const senderPowerLevel = getPowerLevel(event.sender);
|
||||
const powerLevelTag = getPowerLevelTag(senderPowerLevel);
|
||||
const tagColor = powerLevelTag?.color
|
||||
? accessibleTagColors?.get(powerLevelTag.color)
|
||||
: undefined;
|
||||
const tagIconSrc = powerLevelTag?.icon
|
||||
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
|
||||
: undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
key={event.event_id}
|
||||
|
|
@ -212,7 +248,14 @@ export function SearchResultGroup({
|
|||
userId={event.sender}
|
||||
src={
|
||||
senderAvatarMxc
|
||||
? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
|
||||
? mxcUrlToHttp(
|
||||
mx,
|
||||
senderAvatarMxc,
|
||||
useAuthentication,
|
||||
48,
|
||||
48,
|
||||
'crop'
|
||||
) ?? undefined
|
||||
: undefined
|
||||
}
|
||||
alt={displayName}
|
||||
|
|
@ -224,11 +267,14 @@ export function SearchResultGroup({
|
|||
>
|
||||
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
|
||||
<Box gap="200" alignItems="Baseline">
|
||||
<Username style={{ color: colorMXID(event.sender) }}>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username style={{ color: usernameColor }}>
|
||||
<Text as="span" truncate>
|
||||
<b>{displayName}</b>
|
||||
<UsernameBold>{displayName}</UsernameBold>
|
||||
</Text>
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time ts={event.origin_server_ts} />
|
||||
</Box>
|
||||
<Box shrink="No" gap="200" alignItems="Center">
|
||||
|
|
@ -244,11 +290,14 @@ export function SearchResultGroup({
|
|||
</Box>
|
||||
{replyEventId && (
|
||||
<Reply
|
||||
mx={mx}
|
||||
room={room}
|
||||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenClick}
|
||||
getPowerLevel={getPowerLevel}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor}
|
||||
/>
|
||||
)}
|
||||
{renderMatrixEvent(event.type, false, event, displayName, getContent)}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ import { useRoomAvatar, useRoomJoinRule, useRoomName } from '../../hooks/useRoom
|
|||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||
import { General } from './general';
|
||||
import { Members } from './members';
|
||||
import { EmojisStickers } from './emojis-stickers';
|
||||
import { Members } from '../common-settings/members';
|
||||
import { EmojisStickers } from '../common-settings/emojis-stickers';
|
||||
import { Permissions } from './permissions';
|
||||
import { RoomSettingsPage } from '../../state/roomSettings';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import { DeveloperTools } from './developer-tools';
|
||||
import { DeveloperTools } from '../common-settings/developer-tools';
|
||||
|
||||
type RoomSettingsMenuItem = {
|
||||
page: RoomSettingsPage;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import React from 'react';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { RoomProfile } from './RoomProfile';
|
||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { RoomEncryption } from './RoomEncryption';
|
||||
import { RoomHistoryVisibility } from './RoomHistoryVisibility';
|
||||
import { RoomJoinRules } from './RoomJoinRules';
|
||||
import { RoomLocalAddresses, RoomPublishedAddresses } from './RoomAddress';
|
||||
import { RoomPublish } from './RoomPublish';
|
||||
import { RoomUpgrade } from './RoomUpgrade';
|
||||
import {
|
||||
RoomProfile,
|
||||
RoomEncryption,
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
RoomLocalAddresses,
|
||||
RoomPublishedAddresses,
|
||||
RoomPublish,
|
||||
RoomUpgrade,
|
||||
} from '../../common-settings/general';
|
||||
|
||||
type GeneralProps = {
|
||||
requestClose: () => void;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { Powers } from './Powers';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { PowersEditor } from './PowersEditor';
|
||||
import { PermissionGroups } from './PermissionGroups';
|
||||
import { usePermissionGroups } from './usePermissionItems';
|
||||
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
|
||||
|
||||
type PermissionsProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -21,6 +20,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
|
|||
StateEvent.PowerLevelTags,
|
||||
getPowerLevel(mx.getSafeUserId())
|
||||
);
|
||||
const permissionGroups = usePermissionGroups();
|
||||
|
||||
const [powerEditor, setPowerEditor] = useState(false);
|
||||
|
||||
|
|
@ -55,8 +55,9 @@ export function Permissions({ requestClose }: PermissionsProps) {
|
|||
<Powers
|
||||
powerLevels={powerLevels}
|
||||
onEdit={canEditPowers ? handleEditPowers : undefined}
|
||||
permissionGroups={permissionGroups}
|
||||
/>
|
||||
<PermissionGroups powerLevels={powerLevels} />
|
||||
<PermissionGroups powerLevels={powerLevels} permissionGroups={permissionGroups} />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
import { PermissionLocation } from '../../../hooks/usePowerLevels';
|
||||
import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||
|
||||
export type PermissionItem = {
|
||||
location: PermissionLocation;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type PermissionGroup = {
|
||||
name: string;
|
||||
items: PermissionItem[];
|
||||
};
|
||||
import { PermissionGroup } from '../../common-settings/permissions';
|
||||
|
||||
export const usePermissionGroups = (): PermissionGroup[] => {
|
||||
const groups: PermissionGroup[] = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ import {
|
|||
getImageMsgContent,
|
||||
getVideoMsgContent,
|
||||
} from './msgContent';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getMemberDisplayName, getMentionContent, trimReplyFromBody } from '../../utils/room';
|
||||
import { CommandAutocomplete } from './CommandAutocomplete';
|
||||
import { Command, SHRUG, TABLEFLIP, UNFLIP, useCommands } from '../../hooks/useCommands';
|
||||
|
|
@ -109,26 +108,44 @@ import { ReplyLayout, ThreadIndicator } from '../../components/message';
|
|||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
|
||||
import { powerLevelAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { useIsDirectRoom } from '../../hooks/useRoom';
|
||||
|
||||
interface RoomInputProps {
|
||||
editor: Editor;
|
||||
fileDropContainerRef: RefObject<HTMLElement>;
|
||||
roomId: string;
|
||||
room: Room;
|
||||
getPowerLevelTag: GetPowerLevelTag;
|
||||
accessibleTagColors: Map<string, string>;
|
||||
}
|
||||
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
({ editor, fileDropContainerRef, roomId, room }, ref) => {
|
||||
({ editor, fileDropContainerRef, roomId, room, getPowerLevelTag, accessibleTagColors }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const direct = useIsDirectRoom();
|
||||
const commands = useCommands(mx, room);
|
||||
const emojiBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
|
||||
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
|
||||
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
|
||||
const replyUserID = replyDraft?.userId;
|
||||
|
||||
const replyPowerTag = getPowerLevelTag(powerLevelAPI.getPowerLevel(powerLevels, replyUserID));
|
||||
const replyPowerColor = replyPowerTag.color
|
||||
? accessibleTagColors.get(replyPowerTag.color)
|
||||
: undefined;
|
||||
const replyUsernameColor =
|
||||
legacyUsernameColor || direct ? colorMXID(replyUserID ?? '') : replyPowerColor;
|
||||
|
||||
const [uploadBoard, setUploadBoard] = useState(true);
|
||||
const [selectedFiles, setSelectedFiles] = useAtom(roomIdToUploadItemsAtomFamily(roomId));
|
||||
const uploadFamilyObserverAtom = createUploadFamilyObserverAtom(
|
||||
|
|
@ -348,7 +365,10 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||
(evt) => {
|
||||
if ((isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) && !evt.nativeEvent.isComposing) {
|
||||
if (
|
||||
(isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) &&
|
||||
!evt.nativeEvent.isComposing
|
||||
) {
|
||||
evt.preventDefault();
|
||||
submit();
|
||||
}
|
||||
|
|
@ -526,7 +546,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
<Box direction="Column">
|
||||
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
||||
<ReplyLayout
|
||||
userColor={colorMXID(replyDraft.userId)}
|
||||
userColor={replyUsernameColor}
|
||||
username={
|
||||
<Text size="T300" truncate>
|
||||
<b>
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
|||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
|
||||
import { useIsDirectRoom } from '../../hooks/useRoom';
|
||||
|
||||
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
|
||||
({ position, className, ...props }, ref) => (
|
||||
|
|
@ -220,6 +222,8 @@ type RoomTimelineProps = {
|
|||
eventId?: string;
|
||||
roomInputRef: RefObject<HTMLElement>;
|
||||
editor: Editor;
|
||||
getPowerLevelTag: GetPowerLevelTag;
|
||||
accessibleTagColors: Map<string, string>;
|
||||
};
|
||||
|
||||
const PAGINATION_LIMIT = 80;
|
||||
|
|
@ -422,12 +426,21 @@ const getRoomUnreadInfo = (room: Room, scrollTo = false) => {
|
|||
};
|
||||
};
|
||||
|
||||
export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
|
||||
export function RoomTimeline({
|
||||
room,
|
||||
eventId,
|
||||
roomInputRef,
|
||||
editor,
|
||||
getPowerLevelTag,
|
||||
accessibleTagColors,
|
||||
}: RoomTimelineProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
|
||||
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const direct = useIsDirectRoom();
|
||||
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
|
||||
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
|
|
@ -443,11 +456,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const powerLevels = usePowerLevelsContext();
|
||||
const { canDoAction, canSendEvent, canSendStateEvent, getPowerLevel } =
|
||||
usePowerLevelsAPI(powerLevels);
|
||||
|
||||
const myPowerLevel = getPowerLevel(mx.getUserId() ?? '');
|
||||
const canRedact = canDoAction('redact', myPowerLevel);
|
||||
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
|
||||
const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, myPowerLevel);
|
||||
const [editId, setEditId] = useState<string>();
|
||||
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
|
|
@ -996,6 +1011,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent()) as GetContentCallback;
|
||||
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderPowerLevel = getPowerLevel(mEvent.getSender());
|
||||
const senderDisplayName =
|
||||
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
|
||||
|
||||
|
|
@ -1029,6 +1045,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenReply}
|
||||
getPowerLevel={getPowerLevel}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1045,6 +1065,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
)
|
||||
}
|
||||
hideReadReceipts={hideActivity}
|
||||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
>
|
||||
{mEvent.isRedacted() ? (
|
||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||
|
|
@ -1071,6 +1094,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const hasReactions = reactions && reactions.length > 0;
|
||||
const { replyEventId, threadRootId } = mEvent;
|
||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||
const senderPowerLevel = getPowerLevel(mEvent.getSender());
|
||||
|
||||
return (
|
||||
<Message
|
||||
|
|
@ -1102,6 +1126,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenReply}
|
||||
getPowerLevel={getPowerLevel}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1118,6 +1146,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
)
|
||||
}
|
||||
hideReadReceipts={hideActivity}
|
||||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
>
|
||||
<EncryptedContent mEvent={mEvent}>
|
||||
{() => {
|
||||
|
|
@ -1181,6 +1212,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
|
||||
const hasReactions = reactions && reactions.length > 0;
|
||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||
const senderPowerLevel = getPowerLevel(mEvent.getSender());
|
||||
|
||||
return (
|
||||
<Message
|
||||
|
|
@ -1215,6 +1247,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
)
|
||||
}
|
||||
hideReadReceipts={hideActivity}
|
||||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
>
|
||||
{mEvent.isRedacted() ? (
|
||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import { editableActiveElement } from '../../utils/dom';
|
|||
import navigation from '../../../client/state/navigation';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { useAccessibleTagColors, usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
|
||||
const FN_KEYS_REGEX = /^F\d+$/;
|
||||
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
||||
|
|
@ -74,6 +76,10 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
|||
? canSendEvent(EventType.RoomMessage, getPowerLevel(myUserId))
|
||||
: false;
|
||||
|
||||
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const theme = useTheme();
|
||||
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
|
||||
|
||||
useKeyDown(
|
||||
window,
|
||||
useCallback(
|
||||
|
|
@ -103,6 +109,8 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
|||
eventId={eventId}
|
||||
roomInputRef={roomInputRef}
|
||||
editor={editor}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
/>
|
||||
<RoomViewTyping room={room} />
|
||||
</Box>
|
||||
|
|
@ -123,6 +131,8 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
|||
roomId={roomId}
|
||||
fileDropContainerRef={roomViewRef}
|
||||
ref={roomInputRef}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
/>
|
||||
)}
|
||||
{!canMessage && (
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
PopOut,
|
||||
RectCords,
|
||||
Badge,
|
||||
Spinner,
|
||||
} from 'folds';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { JoinRule, Room } from 'matrix-js-sdk';
|
||||
|
|
@ -58,6 +59,12 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
|||
import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents';
|
||||
import { RoomPinMenu } from './room-pin-menu';
|
||||
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
|
||||
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
|
||||
import {
|
||||
getRoomNotificationMode,
|
||||
getRoomNotificationModeIcon,
|
||||
useRoomsNotificationPreferencesContext,
|
||||
} from '../../hooks/useRoomsNotificationPreferences';
|
||||
|
||||
type RoomMenuProps = {
|
||||
room: Room;
|
||||
|
|
@ -70,6 +77,8 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
|||
const powerLevels = usePowerLevelsContext();
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
markAsRead(mx, room.roomId, hideActivity);
|
||||
|
|
@ -109,6 +118,27 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
|||
Mark as Read
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<RoomNotificationModeSwitcher roomId={room.roomId} value={notificationMode}>
|
||||
{(handleOpen, opened, changing) => (
|
||||
<MenuItem
|
||||
size="300"
|
||||
after={
|
||||
changing ? (
|
||||
<Spinner size="100" variant="Secondary" />
|
||||
) : (
|
||||
<Icon size="100" src={getRoomNotificationModeIcon(notificationMode)} />
|
||||
)
|
||||
}
|
||||
radii="300"
|
||||
aria-pressed={opened}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Notifications
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
</RoomNotificationModeSwitcher>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ import {
|
|||
ModernLayout,
|
||||
Time,
|
||||
Username,
|
||||
UsernameBold,
|
||||
} from '../../../components/message';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import {
|
||||
canEditEvent,
|
||||
getEventEdits,
|
||||
|
|
@ -76,6 +76,9 @@ import { getViaServers } from '../../../plugins/via-servers';
|
|||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags';
|
||||
import { PowerIcon } from '../../../components/power';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
|
||||
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
|
||||
|
||||
|
|
@ -672,6 +675,9 @@ export type MessageProps = {
|
|||
reply?: ReactNode;
|
||||
reactions?: ReactNode;
|
||||
hideReadReceipts?: boolean;
|
||||
powerLevelTag?: PowerLevelTag;
|
||||
accessibleTagColors?: Map<string, string>;
|
||||
legacyUsernameColor?: boolean;
|
||||
};
|
||||
export const Message = as<'div', MessageProps>(
|
||||
(
|
||||
|
|
@ -697,6 +703,9 @@ export const Message = as<'div', MessageProps>(
|
|||
reply,
|
||||
reactions,
|
||||
hideReadReceipts,
|
||||
powerLevelTag,
|
||||
accessibleTagColors,
|
||||
legacyUsernameColor,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
|
|
@ -715,6 +724,15 @@ export const Message = as<'div', MessageProps>(
|
|||
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
|
||||
const senderAvatarMxc = getMemberAvatarMxc(room, senderId);
|
||||
|
||||
const tagColor = powerLevelTag?.color
|
||||
? accessibleTagColors?.get(powerLevelTag.color)
|
||||
: undefined;
|
||||
const tagIconSrc = powerLevelTag?.icon
|
||||
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
|
||||
: undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor;
|
||||
|
||||
const headerJSX = !collapse && (
|
||||
<Box
|
||||
gap="300"
|
||||
|
|
@ -723,17 +741,24 @@ export const Message = as<'div', MessageProps>(
|
|||
alignItems="Baseline"
|
||||
grow="Yes"
|
||||
>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username
|
||||
as="button"
|
||||
style={{ color: colorMXID(senderId) }}
|
||||
style={{ color: usernameColor }}
|
||||
data-user-id={senderId}
|
||||
onContextMenu={onUserClick}
|
||||
onClick={onUsernameClick}
|
||||
>
|
||||
<Text as="span" size={messageLayout === MessageLayout.Bubble ? 'T300' : 'T400'} truncate>
|
||||
<b>{senderDisplayName}</b>
|
||||
<Text
|
||||
as="span"
|
||||
size={messageLayout === MessageLayout.Bubble ? 'T300' : 'T400'}
|
||||
truncate
|
||||
>
|
||||
<UsernameBold>{senderDisplayName}</UsernameBold>
|
||||
</Text>
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Box shrink="No" gap="100">
|
||||
{messageLayout === MessageLayout.Modern && hover && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
Reply,
|
||||
Time,
|
||||
Username,
|
||||
UsernameBold,
|
||||
} from '../../../components/message';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
|
|
@ -49,7 +50,6 @@ import {
|
|||
getStateEvent,
|
||||
} from '../../../utils/room';
|
||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
|
||||
import {
|
||||
|
|
@ -72,6 +72,15 @@ import { VirtualTile } from '../../../components/virtualizer';
|
|||
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../../hooks/usePowerLevels';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { ContainerColor } from '../../../styles/ContainerColor.css';
|
||||
import {
|
||||
getTagIconSrc,
|
||||
useAccessibleTagColors,
|
||||
usePowerLevelTags,
|
||||
} from '../../../hooks/usePowerLevelTags';
|
||||
import { useTheme } from '../../../hooks/useTheme';
|
||||
import { PowerIcon } from '../../../components/power';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import { useIsDirectRoom } from '../../../hooks/useRoom';
|
||||
|
||||
type PinnedMessageProps = {
|
||||
room: Room;
|
||||
|
|
@ -84,6 +93,14 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
const pinnedEvent = useRoomEvent(room, eventId);
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const mx = useMatrixClient();
|
||||
const direct = useIsDirectRoom();
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
|
||||
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const theme = useTheme();
|
||||
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
|
||||
|
||||
const [unpinState, unpin] = useAsyncCallback(
|
||||
useCallback(() => {
|
||||
|
|
@ -93,7 +110,7 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
pinned: content.pinned.filter((id) => id !== eventId),
|
||||
};
|
||||
|
||||
return mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents, newContent);
|
||||
return mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents as any, newContent);
|
||||
}, [room, eventId, mx])
|
||||
);
|
||||
|
||||
|
|
@ -148,6 +165,16 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender;
|
||||
const senderAvatarMxc = getMemberAvatarMxc(room, sender);
|
||||
const getContent = (() => pinnedEvent.getContent()) as GetContentCallback;
|
||||
|
||||
const senderPowerLevel = getPowerLevel(sender);
|
||||
const powerLevelTag = getPowerLevelTag(senderPowerLevel);
|
||||
const tagColor = powerLevelTag?.color ? accessibleTagColors?.get(powerLevelTag.color) : undefined;
|
||||
const tagIconSrc = powerLevelTag?.icon
|
||||
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
|
||||
: undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor || direct ? colorMXID(sender) : tagColor;
|
||||
|
||||
return (
|
||||
<ModernLayout
|
||||
before={
|
||||
|
|
@ -170,11 +197,14 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
>
|
||||
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
|
||||
<Box gap="200" alignItems="Baseline">
|
||||
<Username style={{ color: colorMXID(sender) }}>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username style={{ color: usernameColor }}>
|
||||
<Text as="span" truncate>
|
||||
<b>{displayName}</b>
|
||||
<UsernameBold>{displayName}</UsernameBold>
|
||||
</Text>
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time ts={pinnedEvent.getTs()} />
|
||||
</Box>
|
||||
{renderOptions()}
|
||||
|
|
@ -185,6 +215,10 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
replyEventId={pinnedEvent.replyEventId}
|
||||
threadRootId={pinnedEvent.threadRootId}
|
||||
onClick={handleOpenClick}
|
||||
getPowerLevel={getPowerLevel}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor}
|
||||
/>
|
||||
)}
|
||||
{renderContent(pinnedEvent.getType(), false, pinnedEvent, displayName, getContent)}
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
|
|||
const disableSetDisplayname = capabilities['m.set_displayname']?.enabled === false;
|
||||
|
||||
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const [displayName, setDisplayName] = useState<string>();
|
||||
const [displayName, setDisplayName] = useState<string>(defaultDisplayName);
|
||||
|
||||
const [changeState, changeDisplayName] = useAsyncCallback(
|
||||
useCallback((name: string) => mx.setDisplayName(name), [mx])
|
||||
|
|
|
|||
|
|
@ -514,6 +514,10 @@ function SelectMessageSpacing() {
|
|||
}
|
||||
|
||||
function Messages() {
|
||||
const [legacyUsernameColor, setLegacyUsernameColor] = useSetting(
|
||||
settingsAtom,
|
||||
'legacyUsernameColor'
|
||||
);
|
||||
const [hideMembershipEvents, setHideMembershipEvents] = useSetting(
|
||||
settingsAtom,
|
||||
'hideMembershipEvents'
|
||||
|
|
@ -536,6 +540,18 @@ function Messages() {
|
|||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile title="Message Spacing" after={<SelectMessageSpacing />} />
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Legacy Username Color"
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={legacyUsernameColor}
|
||||
onChange={setLegacyUsernameColor}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Hide Membership Change"
|
||||
|
|
|
|||
173
src/app/features/space-settings/SpaceSettings.tsx
Normal file
173
src/app/features/space-settings/SpaceSettings.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Avatar, Box, config, Icon, IconButton, Icons, IconSrc, MenuItem, Text } from 'folds';
|
||||
import { JoinRule } from 'matrix-js-sdk';
|
||||
import { PageNav, PageNavContent, PageNavHeader, PageRoot } from '../../components/page';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useRoomAvatar, useRoomJoinRule, useRoomName } from '../../hooks/useRoomMeta';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||
import { SpaceSettingsPage } from '../../state/spaceSettings';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import { EmojisStickers } from '../common-settings/emojis-stickers';
|
||||
import { Members } from '../common-settings/members';
|
||||
import { DeveloperTools } from '../common-settings/developer-tools';
|
||||
import { General } from './general';
|
||||
import { Permissions } from './permissions';
|
||||
|
||||
type SpaceSettingsMenuItem = {
|
||||
page: SpaceSettingsPage;
|
||||
name: string;
|
||||
icon: IconSrc;
|
||||
};
|
||||
|
||||
const useSpaceSettingsMenuItems = (): SpaceSettingsMenuItem[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
page: SpaceSettingsPage.GeneralPage,
|
||||
name: 'General',
|
||||
icon: Icons.Setting,
|
||||
},
|
||||
{
|
||||
page: SpaceSettingsPage.MembersPage,
|
||||
name: 'Members',
|
||||
icon: Icons.User,
|
||||
},
|
||||
{
|
||||
page: SpaceSettingsPage.PermissionsPage,
|
||||
name: 'Permissions',
|
||||
icon: Icons.Lock,
|
||||
},
|
||||
{
|
||||
page: SpaceSettingsPage.EmojisStickersPage,
|
||||
name: 'Emojis & Stickers',
|
||||
icon: Icons.Smile,
|
||||
},
|
||||
{
|
||||
page: SpaceSettingsPage.DeveloperToolsPage,
|
||||
name: 'Developer Tools',
|
||||
icon: Icons.Terminal,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
type SpaceSettingsProps = {
|
||||
initialPage?: SpaceSettingsPage;
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps) {
|
||||
const room = useRoom();
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
const roomAvatar = useRoomAvatar(room, mDirects.has(room.roomId));
|
||||
const roomName = useRoomName(room);
|
||||
const joinRuleContent = useRoomJoinRule(room);
|
||||
|
||||
const avatarUrl = roomAvatar
|
||||
? mxcUrlToHttp(mx, roomAvatar, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
const screenSize = useScreenSizeContext();
|
||||
const [activePage, setActivePage] = useState<SpaceSettingsPage | undefined>(() => {
|
||||
if (initialPage) return initialPage;
|
||||
return screenSize === ScreenSize.Mobile ? undefined : SpaceSettingsPage.GeneralPage;
|
||||
});
|
||||
const menuItems = useSpaceSettingsMenuItems();
|
||||
|
||||
const handlePageRequestClose = () => {
|
||||
if (screenSize === ScreenSize.Mobile) {
|
||||
setActivePage(undefined);
|
||||
return;
|
||||
}
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<PageRoot
|
||||
nav={
|
||||
screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : (
|
||||
<PageNav size="300">
|
||||
<PageNavHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Avatar size="200" radii="300">
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={avatarUrl}
|
||||
alt={roomName}
|
||||
renderFallback={() => (
|
||||
<RoomIcon
|
||||
space
|
||||
size="50"
|
||||
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
<Text size="H4" truncate>
|
||||
{roomName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<IconButton onClick={requestClose} variant="Background">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</PageNavHeader>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<PageNavContent>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
{menuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
variant="Background"
|
||||
radii="400"
|
||||
aria-pressed={activePage === item.page}
|
||||
before={<Icon src={item.icon} size="100" filled={activePage === item.page} />}
|
||||
onClick={() => setActivePage(item.page)}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontWeight: activePage === item.page ? config.fontWeight.W600 : undefined,
|
||||
}}
|
||||
size="T300"
|
||||
truncate
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
</PageNavContent>
|
||||
</Box>
|
||||
</PageNav>
|
||||
)
|
||||
}
|
||||
>
|
||||
{activePage === SpaceSettingsPage.GeneralPage && (
|
||||
<General requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
{activePage === SpaceSettingsPage.MembersPage && (
|
||||
<Members requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
{activePage === SpaceSettingsPage.PermissionsPage && (
|
||||
<Permissions requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
{activePage === SpaceSettingsPage.EmojisStickersPage && (
|
||||
<EmojisStickers requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
{activePage === SpaceSettingsPage.DeveloperToolsPage && (
|
||||
<DeveloperTools requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
</PageRoot>
|
||||
);
|
||||
}
|
||||
39
src/app/features/space-settings/SpaceSettingsRenderer.tsx
Normal file
39
src/app/features/space-settings/SpaceSettingsRenderer.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { SpaceSettings } from './SpaceSettings';
|
||||
import { Modal500 } from '../../components/Modal500';
|
||||
import { useCloseSpaceSettings, useSpaceSettingsState } from '../../state/hooks/spaceSettings';
|
||||
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
|
||||
import { SpaceSettingsState } from '../../state/spaceSettings';
|
||||
import { RoomProvider } from '../../hooks/useRoom';
|
||||
import { SpaceProvider } from '../../hooks/useSpace';
|
||||
|
||||
type RenderSettingsProps = {
|
||||
state: SpaceSettingsState;
|
||||
};
|
||||
function RenderSettings({ state }: RenderSettingsProps) {
|
||||
const { roomId, spaceId, page } = state;
|
||||
const closeSettings = useCloseSpaceSettings();
|
||||
const allJoinedRooms = useAllJoinedRoomsSet();
|
||||
const getRoom = useGetRoom(allJoinedRooms);
|
||||
const room = getRoom(roomId);
|
||||
const space = spaceId ? getRoom(spaceId) : undefined;
|
||||
|
||||
if (!room) return null;
|
||||
|
||||
return (
|
||||
<Modal500 requestClose={closeSettings}>
|
||||
<SpaceProvider value={space ?? null}>
|
||||
<RoomProvider value={room}>
|
||||
<SpaceSettings initialPage={page} requestClose={closeSettings} />
|
||||
</RoomProvider>
|
||||
</SpaceProvider>
|
||||
</Modal500>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpaceSettingsRenderer() {
|
||||
const state = useSpaceSettingsState();
|
||||
|
||||
if (!state) return null;
|
||||
return <RenderSettings state={state} />;
|
||||
}
|
||||
63
src/app/features/space-settings/general/General.tsx
Normal file
63
src/app/features/space-settings/general/General.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import {
|
||||
RoomProfile,
|
||||
RoomJoinRules,
|
||||
RoomLocalAddresses,
|
||||
RoomPublishedAddresses,
|
||||
RoomPublish,
|
||||
RoomUpgrade,
|
||||
} from '../../common-settings/general';
|
||||
|
||||
type GeneralProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function General({ requestClose }: GeneralProps) {
|
||||
const room = useRoom();
|
||||
const powerLevels = usePowerLevels(room);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
General
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="Surface">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent>
|
||||
<Box direction="Column" gap="700">
|
||||
<RoomProfile powerLevels={powerLevels} />
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Options</Text>
|
||||
<RoomJoinRules powerLevels={powerLevels} />
|
||||
<RoomPublish powerLevels={powerLevels} />
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Addresses</Text>
|
||||
<RoomPublishedAddresses powerLevels={powerLevels} />
|
||||
<RoomLocalAddresses powerLevels={powerLevels} />
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Advance Options</Text>
|
||||
<RoomUpgrade powerLevels={powerLevels} requestClose={requestClose} />
|
||||
</Box>
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
1
src/app/features/space-settings/general/index.ts
Normal file
1
src/app/features/space-settings/general/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './General';
|
||||
2
src/app/features/space-settings/index.ts
Normal file
2
src/app/features/space-settings/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './SpaceSettings';
|
||||
export * from './SpaceSettingsRenderer';
|
||||
67
src/app/features/space-settings/permissions/Permissions.tsx
Normal file
67
src/app/features/space-settings/permissions/Permissions.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { usePermissionGroups } from './usePermissionItems';
|
||||
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
|
||||
|
||||
type PermissionsProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function Permissions({ requestClose }: PermissionsProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
|
||||
const canEditPowers = canSendStateEvent(
|
||||
StateEvent.PowerLevelTags,
|
||||
getPowerLevel(mx.getSafeUserId())
|
||||
);
|
||||
const permissionGroups = usePermissionGroups();
|
||||
|
||||
const [powerEditor, setPowerEditor] = useState(false);
|
||||
|
||||
const handleEditPowers = () => {
|
||||
setPowerEditor(true);
|
||||
};
|
||||
|
||||
if (canEditPowers && powerEditor) {
|
||||
return <PowersEditor powerLevels={powerLevels} requestClose={() => setPowerEditor(false)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
Permissions
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="Surface">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent>
|
||||
<Box direction="Column" gap="700">
|
||||
<Powers
|
||||
powerLevels={powerLevels}
|
||||
onEdit={canEditPowers ? handleEditPowers : undefined}
|
||||
permissionGroups={permissionGroups}
|
||||
/>
|
||||
<PermissionGroups powerLevels={powerLevels} permissionGroups={permissionGroups} />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
1
src/app/features/space-settings/permissions/index.ts
Normal file
1
src/app/features/space-settings/permissions/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './Permissions';
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import { useMemo } from 'react';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { PermissionGroup } from '../../common-settings/permissions';
|
||||
|
||||
export const usePermissionGroups = (): PermissionGroup[] => {
|
||||
const groups: PermissionGroup[] = useMemo(() => {
|
||||
const messagesGroup: PermissionGroup = {
|
||||
name: 'Manage',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.SpaceChild,
|
||||
},
|
||||
name: 'Manage space rooms',
|
||||
},
|
||||
{
|
||||
location: {},
|
||||
name: 'Message Events',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const moderationGroup: PermissionGroup = {
|
||||
name: 'Moderation',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
action: true,
|
||||
key: 'invite',
|
||||
},
|
||||
name: 'Invite',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
action: true,
|
||||
key: 'kick',
|
||||
},
|
||||
name: 'Kick',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
action: true,
|
||||
key: 'ban',
|
||||
},
|
||||
name: 'Ban',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const roomOverviewGroup: PermissionGroup = {
|
||||
name: 'Space Overview',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomAvatar,
|
||||
},
|
||||
name: 'Space Avatar',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomName,
|
||||
},
|
||||
name: 'Space Name',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomTopic,
|
||||
},
|
||||
name: 'Space Topic',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const roomSettingsGroup: PermissionGroup = {
|
||||
name: 'Settings',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomJoinRules,
|
||||
},
|
||||
name: 'Change Space Access',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomCanonicalAlias,
|
||||
},
|
||||
name: 'Publish Address',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomPowerLevels,
|
||||
},
|
||||
name: 'Change All Permission',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.PowerLevelTags,
|
||||
},
|
||||
name: 'Edit Power Levels',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomTombstone,
|
||||
},
|
||||
name: 'Upgrade Space',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
},
|
||||
name: 'Other Settings',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const otherSettingsGroup: PermissionGroup = {
|
||||
name: 'Other',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomServerAcl,
|
||||
},
|
||||
name: 'Change Server ACLs',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return [
|
||||
messagesGroup,
|
||||
moderationGroup,
|
||||
roomOverviewGroup,
|
||||
roomSettingsGroup,
|
||||
otherSettingsGroup,
|
||||
];
|
||||
}, []);
|
||||
|
||||
return groups;
|
||||
};
|
||||
6
src/app/features/space-settings/styles.css.ts
Normal file
6
src/app/features/space-settings/styles.css.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { config } from 'folds';
|
||||
|
||||
export const SequenceCardStyle = style({
|
||||
padding: config.space.S300,
|
||||
});
|
||||
|
|
@ -4,6 +4,8 @@ import { IPowerLevels } from './usePowerLevels';
|
|||
import { useStateEvent } from './useStateEvent';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { IImageInfo } from '../../types/matrix/common';
|
||||
import { ThemeKind } from './useTheme';
|
||||
import { accessibleColor } from '../plugins/color';
|
||||
|
||||
export type PowerLevelTagIcon = {
|
||||
key?: string;
|
||||
|
|
@ -63,7 +65,7 @@ const DEFAULT_TAGS: PowerLevelTags = {
|
|||
},
|
||||
100: {
|
||||
name: 'Admin',
|
||||
color: '#a000e4',
|
||||
color: '#0088ff',
|
||||
},
|
||||
50: {
|
||||
name: 'Moderator',
|
||||
|
|
@ -71,9 +73,11 @@ const DEFAULT_TAGS: PowerLevelTags = {
|
|||
},
|
||||
0: {
|
||||
name: 'Member',
|
||||
color: '#91cfdf',
|
||||
},
|
||||
[-1]: {
|
||||
name: 'Muted',
|
||||
color: '#888888',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -152,3 +156,24 @@ export const getTagIconSrc = (
|
|||
icon?.key?.startsWith('mxc://')
|
||||
? mx.mxcUrlToHttp(icon.key, 96, 96, 'scale', undefined, undefined, useAuthentication) ?? '🌻'
|
||||
: icon?.key;
|
||||
|
||||
export const useAccessibleTagColors = (
|
||||
themeKind: ThemeKind,
|
||||
powerLevelTags: PowerLevelTags
|
||||
): Map<string, string> => {
|
||||
const accessibleColors: Map<string, string> = useMemo(() => {
|
||||
const colors: Map<string, string> = new Map();
|
||||
|
||||
getPowers(powerLevelTags).forEach((power) => {
|
||||
const tag = powerLevelTags[power];
|
||||
const { color } = tag;
|
||||
if (!color) return;
|
||||
|
||||
colors.set(color, accessibleColor(themeKind, color));
|
||||
});
|
||||
|
||||
return colors;
|
||||
}, [powerLevelTags, themeKind]);
|
||||
|
||||
return accessibleColors;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,3 +10,13 @@ export function useRoom(): Room {
|
|||
if (!room) throw new Error('Room not provided!');
|
||||
return room;
|
||||
}
|
||||
|
||||
const IsDirectRoomContext = createContext<boolean>(false);
|
||||
|
||||
export const IsDirectRoomProvider = IsDirectRoomContext.Provider;
|
||||
|
||||
export const useIsDirectRoom = () => {
|
||||
const direct = useContext(IsDirectRoomContext);
|
||||
|
||||
return direct;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { lightTheme } from 'folds';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
||||
import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
|
||||
export enum ThemeKind {
|
||||
Light = 'light',
|
||||
|
|
@ -72,3 +74,37 @@ export const useSystemThemeKind = (): ThemeKind => {
|
|||
|
||||
return themeKind;
|
||||
};
|
||||
|
||||
export const useActiveTheme = (): Theme => {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
const themes = useThemes();
|
||||
const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||
const [themeId] = useSetting(settingsAtom, 'themeId');
|
||||
const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
|
||||
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
||||
|
||||
if (!systemTheme) {
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
|
||||
return selectedTheme;
|
||||
}
|
||||
|
||||
const selectedTheme =
|
||||
systemThemeKind === ThemeKind.Dark
|
||||
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
|
||||
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
|
||||
|
||||
return selectedTheme;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<Theme | null>(null);
|
||||
export const ThemeContextProvider = ThemeContext.Provider;
|
||||
|
||||
export const useTheme = (): Theme => {
|
||||
const theme = useContext(ThemeContext);
|
||||
if (!theme) {
|
||||
throw new Error('No theme provided!');
|
||||
}
|
||||
|
||||
return theme;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,511 +0,0 @@
|
|||
import React, { useState, useMemo, useReducer, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EventTimeline } from 'matrix-js-sdk';
|
||||
import './ImagePack.scss';
|
||||
|
||||
import { openReusableDialog } from '../../../client/action/navigation';
|
||||
import { suffixRename } from '../../../util/common';
|
||||
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Checkbox from '../../atoms/button/Checkbox';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import { ImagePack as ImagePackBuilder } from '../../organisms/emoji-board/custom-emoji';
|
||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import ImagePackProfile from './ImagePackProfile';
|
||||
import ImagePackItem from './ImagePackItem';
|
||||
import ImagePackUpload from './ImagePackUpload';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { getStateEvent } from '../../utils/room';
|
||||
|
||||
const renameImagePackItem = (shortcode) =>
|
||||
new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">
|
||||
Rename
|
||||
</Text>,
|
||||
(requestClose) => (
|
||||
<div style={{ padding: 'var(--sp-normal)' }}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const sc = e.target.shortcode.value;
|
||||
if (sc.trim() === '') return;
|
||||
isCompleted = true;
|
||||
resolve(sc.trim());
|
||||
requestClose();
|
||||
}}
|
||||
>
|
||||
<Input value={shortcode} name="shortcode" label="Shortcode" autoFocus required />
|
||||
<div style={{ height: 'var(--sp-normal)' }} />
|
||||
<Button variant="primary" type="submit">
|
||||
Rename
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
),
|
||||
() => {
|
||||
if (!isCompleted) resolve(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function getUsage(usage) {
|
||||
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
|
||||
if (usage.includes('emoticon')) return 'emoticon';
|
||||
if (usage.includes('sticker')) return 'sticker';
|
||||
|
||||
return 'both';
|
||||
}
|
||||
|
||||
function isGlobalPack(mx, roomId, stateKey) {
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
||||
if (typeof globalContent !== 'object') return false;
|
||||
|
||||
const { rooms } = globalContent;
|
||||
if (typeof rooms !== 'object') return false;
|
||||
|
||||
return rooms[roomId]?.[stateKey] !== undefined;
|
||||
}
|
||||
|
||||
function useRoomImagePack(roomId, stateKey) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const pack = useMemo(() => {
|
||||
const packEvent = getStateEvent(room, 'im.ponies.room_emotes', stateKey);
|
||||
return ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent());
|
||||
}, [room, stateKey]);
|
||||
|
||||
const sendPackContent = (content) => {
|
||||
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
|
||||
};
|
||||
|
||||
return {
|
||||
pack,
|
||||
sendPackContent,
|
||||
};
|
||||
}
|
||||
|
||||
function useUserImagePack() {
|
||||
const mx = useMatrixClient();
|
||||
const pack = useMemo(() => {
|
||||
const packEvent = mx.getAccountData('im.ponies.user_emotes');
|
||||
return ImagePackBuilder.parsePack(
|
||||
mx.getUserId(),
|
||||
packEvent?.getContent() ?? {
|
||||
pack: { display_name: 'Personal' },
|
||||
images: {},
|
||||
}
|
||||
);
|
||||
}, [mx]);
|
||||
|
||||
const sendPackContent = (content) => {
|
||||
mx.setAccountData('im.ponies.user_emotes', content);
|
||||
};
|
||||
|
||||
return {
|
||||
pack,
|
||||
sendPackContent,
|
||||
};
|
||||
}
|
||||
|
||||
function useImagePackHandles(pack, sendPackContent) {
|
||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
|
||||
const getNewKey = (key) => {
|
||||
if (typeof key !== 'string') return undefined;
|
||||
let newKey = key?.replace(/\s/g, '_');
|
||||
if (pack.getImages().get(newKey)) {
|
||||
newKey = suffixRename(newKey, (suffixedKey) => pack.getImages().get(suffixedKey));
|
||||
}
|
||||
return newKey;
|
||||
};
|
||||
|
||||
const handleAvatarChange = (url) => {
|
||||
pack.setAvatarUrl(url);
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleEditProfile = (name, attribution) => {
|
||||
pack.setDisplayName(name);
|
||||
pack.setAttribution(attribution);
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleUsageChange = (newUsage) => {
|
||||
const usage = [];
|
||||
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||
pack.setUsage(usage);
|
||||
pack.getImages().forEach((img) => pack.setImageUsage(img.shortcode, undefined));
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
const handleRenameItem = async (key) => {
|
||||
const newKey = getNewKey(await renameImagePackItem(key));
|
||||
|
||||
if (!newKey || newKey === key) return;
|
||||
pack.updateImageKey(key, newKey);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleDeleteItem = async (key) => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Delete',
|
||||
`Are you sure that you want to delete "${key}"?`,
|
||||
'Delete',
|
||||
'danger'
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
pack.removeImage(key);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleUsageItem = (key, newUsage) => {
|
||||
const usage = [];
|
||||
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||
pack.setImageUsage(key, usage);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleAddItem = (key, url) => {
|
||||
const newKey = getNewKey(key);
|
||||
if (!newKey || !url) return;
|
||||
|
||||
pack.addImage(newKey, {
|
||||
url,
|
||||
});
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
return {
|
||||
handleAvatarChange,
|
||||
handleEditProfile,
|
||||
handleUsageChange,
|
||||
handleRenameItem,
|
||||
handleDeleteItem,
|
||||
handleUsageItem,
|
||||
handleAddItem,
|
||||
};
|
||||
}
|
||||
|
||||
function addGlobalImagePack(mx, roomId, stateKey) {
|
||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||
if (!content.rooms) content.rooms = {};
|
||||
if (!content.rooms[roomId]) content.rooms[roomId] = {};
|
||||
content.rooms[roomId][stateKey] = {};
|
||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||
}
|
||||
function removeGlobalImagePack(mx, roomId, stateKey) {
|
||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||
if (!content.rooms) return Promise.resolve();
|
||||
if (!content.rooms[roomId]) return Promise.resolve();
|
||||
delete content.rooms[roomId][stateKey];
|
||||
if (Object.keys(content.rooms[roomId]).length === 0) {
|
||||
delete content.rooms[roomId];
|
||||
}
|
||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||
}
|
||||
|
||||
function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
const [viewMore, setViewMore] = useState(false);
|
||||
const [isGlobal, setIsGlobal] = useState(isGlobalPack(mx, roomId, stateKey));
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
|
||||
const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey);
|
||||
|
||||
const {
|
||||
handleAvatarChange,
|
||||
handleEditProfile,
|
||||
handleUsageChange,
|
||||
handleRenameItem,
|
||||
handleDeleteItem,
|
||||
handleUsageItem,
|
||||
handleAddItem,
|
||||
} = useImagePackHandles(pack, sendPackContent);
|
||||
|
||||
const handleGlobalChange = (isG) => {
|
||||
setIsGlobal(isG);
|
||||
if (isG) addGlobalImagePack(mx, roomId, stateKey);
|
||||
else removeGlobalImagePack(mx, roomId, stateKey);
|
||||
};
|
||||
|
||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||
const canChange = room
|
||||
.getLiveTimeline()
|
||||
.getState(EventTimeline.FORWARDS)
|
||||
?.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||
|
||||
const handleDeletePack = async () => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Delete Pack',
|
||||
`Are you sure that you want to delete "${pack.displayName}"?`,
|
||||
'Delete',
|
||||
'danger'
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
handlePackDelete(stateKey);
|
||||
};
|
||||
|
||||
const images = [...pack.images].slice(0, viewMore ? pack.images.size : 2);
|
||||
|
||||
return (
|
||||
<div className="image-pack">
|
||||
<ImagePackProfile
|
||||
avatarUrl={
|
||||
pack.avatarUrl
|
||||
? mx.mxcUrlToHttp(
|
||||
pack.avatarUrl,
|
||||
42,
|
||||
42,
|
||||
'crop',
|
||||
undefined,
|
||||
undefined,
|
||||
useAuthentication
|
||||
)
|
||||
: null
|
||||
}
|
||||
displayName={pack.displayName ?? 'Unknown'}
|
||||
attribution={pack.attribution}
|
||||
usage={getUsage(pack.usage)}
|
||||
onUsageChange={canChange ? handleUsageChange : null}
|
||||
onAvatarChange={canChange ? handleAvatarChange : null}
|
||||
onEditProfile={canChange ? handleEditProfile : null}
|
||||
/>
|
||||
{canChange && <ImagePackUpload onUpload={handleAddItem} />}
|
||||
{images.length === 0 ? null : (
|
||||
<div>
|
||||
<div className="image-pack__header">
|
||||
<Text variant="b3">Image</Text>
|
||||
<Text variant="b3">Shortcode</Text>
|
||||
<Text variant="b3">Usage</Text>
|
||||
</div>
|
||||
{images.map(([shortcode, image]) => (
|
||||
<ImagePackItem
|
||||
key={shortcode}
|
||||
url={mx.mxcUrlToHttp(
|
||||
image.mxc,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
useAuthentication
|
||||
)}
|
||||
shortcode={shortcode}
|
||||
usage={getUsage(image.usage)}
|
||||
onUsageChange={canChange ? handleUsageItem : undefined}
|
||||
onDelete={canChange ? handleDeleteItem : undefined}
|
||||
onRename={canChange ? handleRenameItem : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(pack.images.size > 2 || handlePackDelete) && (
|
||||
<div className="image-pack__footer">
|
||||
{pack.images.size > 2 && (
|
||||
<Button onClick={() => setViewMore(!viewMore)}>
|
||||
{viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
|
||||
</Button>
|
||||
)}
|
||||
{handlePackDelete && (
|
||||
<Button variant="danger" onClick={handleDeletePack}>
|
||||
Delete Pack
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="image-pack__global">
|
||||
<Checkbox variant="positive" onToggle={handleGlobalChange} isActive={isGlobal} />
|
||||
<div>
|
||||
<Text variant="b2">Use globally</Text>
|
||||
<Text variant="b3">Add this pack to your account to use in all rooms.</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePack.defaultProps = {
|
||||
handlePackDelete: null,
|
||||
};
|
||||
ImagePack.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
stateKey: PropTypes.string.isRequired,
|
||||
handlePackDelete: PropTypes.func,
|
||||
};
|
||||
|
||||
function ImagePackUser() {
|
||||
const mx = useMatrixClient();
|
||||
const [viewMore, setViewMore] = useState(false);
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
|
||||
const { pack, sendPackContent } = useUserImagePack();
|
||||
|
||||
const {
|
||||
handleAvatarChange,
|
||||
handleEditProfile,
|
||||
handleUsageChange,
|
||||
handleRenameItem,
|
||||
handleDeleteItem,
|
||||
handleUsageItem,
|
||||
handleAddItem,
|
||||
} = useImagePackHandles(pack, sendPackContent);
|
||||
|
||||
const images = [...pack.images].slice(0, viewMore ? pack.images.size : 2);
|
||||
|
||||
return (
|
||||
<div className="image-pack">
|
||||
<ImagePackProfile
|
||||
avatarUrl={
|
||||
pack.avatarUrl
|
||||
? mx.mxcUrlToHttp(
|
||||
pack.avatarUrl,
|
||||
42,
|
||||
42,
|
||||
'crop',
|
||||
undefined,
|
||||
undefined,
|
||||
useAuthentication
|
||||
)
|
||||
: null
|
||||
}
|
||||
displayName={pack.displayName ?? 'Personal'}
|
||||
attribution={pack.attribution}
|
||||
usage={getUsage(pack.usage)}
|
||||
onUsageChange={handleUsageChange}
|
||||
onAvatarChange={handleAvatarChange}
|
||||
onEditProfile={handleEditProfile}
|
||||
/>
|
||||
<ImagePackUpload onUpload={handleAddItem} />
|
||||
{images.length === 0 ? null : (
|
||||
<div>
|
||||
<div className="image-pack__header">
|
||||
<Text variant="b3">Image</Text>
|
||||
<Text variant="b3">Shortcode</Text>
|
||||
<Text variant="b3">Usage</Text>
|
||||
</div>
|
||||
{images.map(([shortcode, image]) => (
|
||||
<ImagePackItem
|
||||
key={shortcode}
|
||||
url={mx.mxcUrlToHttp(
|
||||
image.mxc,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
useAuthentication
|
||||
)}
|
||||
shortcode={shortcode}
|
||||
usage={getUsage(image.usage)}
|
||||
onUsageChange={handleUsageItem}
|
||||
onDelete={handleDeleteItem}
|
||||
onRename={handleRenameItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{pack.images.size > 2 && (
|
||||
<div className="image-pack__footer">
|
||||
<Button onClick={() => setViewMore(!viewMore)}>
|
||||
{viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useGlobalImagePack() {
|
||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const roomIdToStateKeys = new Map();
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? { rooms: {} };
|
||||
const { rooms } = globalContent;
|
||||
|
||||
Object.keys(rooms).forEach((roomId) => {
|
||||
if (typeof rooms[roomId] !== 'object') return;
|
||||
const room = mx.getRoom(roomId);
|
||||
const stateKeys = Object.keys(rooms[roomId]);
|
||||
if (!room || stateKeys.length === 0) return;
|
||||
roomIdToStateKeys.set(roomId, stateKeys);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleEvent = (event) => {
|
||||
if (event.getType() === 'im.ponies.emote_rooms') forceUpdate();
|
||||
};
|
||||
mx.addListener('accountData', handleEvent);
|
||||
return () => {
|
||||
mx.removeListener('accountData', handleEvent);
|
||||
};
|
||||
}, [mx]);
|
||||
|
||||
return roomIdToStateKeys;
|
||||
}
|
||||
|
||||
function ImagePackGlobal() {
|
||||
const mx = useMatrixClient();
|
||||
const roomIdToStateKeys = useGlobalImagePack();
|
||||
|
||||
const handleChange = (roomId, stateKey) => {
|
||||
removeGlobalImagePack(mx, roomId, stateKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-global">
|
||||
<MenuHeader>Global packs</MenuHeader>
|
||||
<div>
|
||||
{roomIdToStateKeys.size > 0 ? (
|
||||
[...roomIdToStateKeys].map(([roomId, stateKeys]) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
return stateKeys.map((stateKey) => {
|
||||
const data = getStateEvent(room, 'im.ponies.room_emotes', stateKey);
|
||||
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent());
|
||||
if (!pack) return null;
|
||||
return (
|
||||
<div className="image-pack__global" key={pack.id}>
|
||||
<Checkbox
|
||||
variant="positive"
|
||||
onToggle={() => handleChange(roomId, stateKey)}
|
||||
isActive
|
||||
/>
|
||||
<div>
|
||||
<Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
|
||||
<Text variant="b3">{room.name}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})
|
||||
) : (
|
||||
<div className="image-pack-global__empty">
|
||||
<Text>No global packs</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImagePack;
|
||||
|
||||
export { ImagePackUser, ImagePackGlobal };
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
|
||||
.image-pack {
|
||||
&-item {
|
||||
border-top: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
& > *:nth-child(2) {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: var(--sp-normal);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-tight);
|
||||
}
|
||||
|
||||
&__global {
|
||||
padding: var(--sp-normal);
|
||||
padding-top: var(--sp-tight);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.image-pack-global {
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: var(--sp-extra-loose) var(--sp-normal);
|
||||
}
|
||||
& .image-pack__global {
|
||||
padding: 0 var(--sp-normal);
|
||||
padding-bottom: var(--sp-normal);
|
||||
&:first-child {
|
||||
padding-top: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackItem.scss';
|
||||
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||
|
||||
function ImagePackItem({
|
||||
url, shortcode, usage, onUsageChange, onDelete, onRename,
|
||||
}) {
|
||||
const handleUsageSelect = (event) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(event, '.btn-surface'),
|
||||
(closeMenu) => (
|
||||
<ImagePackUsageSelector
|
||||
usage={usage}
|
||||
onSelect={(newUsage) => {
|
||||
onUsageChange(shortcode, newUsage);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-item">
|
||||
<Avatar imageSrc={url} size="extra-small" text={shortcode} bgColor="black" />
|
||||
<div className="image-pack-item__content">
|
||||
<Text>{shortcode}</Text>
|
||||
</div>
|
||||
<div className="image-pack-item__usage">
|
||||
<div className="image-pack-item__btn">
|
||||
{onRename && <IconButton tooltip="Rename" size="extra-small" src={PencilIC} onClick={() => onRename(shortcode)} />}
|
||||
{onDelete && <IconButton tooltip="Delete" size="extra-small" src={BinIC} onClick={() => onDelete(shortcode)} />}
|
||||
</div>
|
||||
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
|
||||
{onUsageChange && <RawIcon src={ChevronBottomIC} size="extra-small" />}
|
||||
<Text variant="b2">
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Both'}
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackItem.defaultProps = {
|
||||
onUsageChange: null,
|
||||
onDelete: null,
|
||||
onRename: null,
|
||||
};
|
||||
ImagePackItem.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
shortcode: PropTypes.string.isRequired,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onUsageChange: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onRename: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ImagePackItem;
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.image-pack-item {
|
||||
margin: 0 var(--sp-normal);
|
||||
padding: var(--sp-tight) 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
& .avatar-container img {
|
||||
object-fit: contain;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
|
||||
&__usage {
|
||||
display: flex;
|
||||
gap: var(--sp-ultra-tight);
|
||||
& button {
|
||||
padding: 6px;
|
||||
}
|
||||
& > button.btn-surface {
|
||||
padding: 6px var(--sp-tight);
|
||||
min-width: 0;
|
||||
@include dir.side(margin, var(--sp-ultra-tight), 0);
|
||||
}
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: none;
|
||||
}
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.image-pack-item__btn {
|
||||
display: flex;
|
||||
gap: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackProfile.scss';
|
||||
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import ImageUpload from '../image-upload/ImageUpload';
|
||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
|
||||
function ImagePackProfile({
|
||||
avatarUrl, displayName, attribution, usage,
|
||||
onUsageChange, onAvatarChange, onEditProfile,
|
||||
}) {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { nameInput, attributionInput } = e.target;
|
||||
const name = nameInput.value.trim() || undefined;
|
||||
const att = attributionInput.value.trim() || undefined;
|
||||
|
||||
onEditProfile(name, att);
|
||||
setIsEdit(false);
|
||||
};
|
||||
|
||||
const handleUsageSelect = (event) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(event, '.btn-surface'),
|
||||
(closeMenu) => (
|
||||
<ImagePackUsageSelector
|
||||
usage={usage}
|
||||
onSelect={(newUsage) => {
|
||||
onUsageChange(newUsage);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-profile">
|
||||
{
|
||||
onAvatarChange
|
||||
? (
|
||||
<ImageUpload
|
||||
bgColor="#555"
|
||||
text={displayName}
|
||||
imageSrc={avatarUrl}
|
||||
size="normal"
|
||||
onUpload={onAvatarChange}
|
||||
onRequestRemove={() => onAvatarChange(undefined)}
|
||||
/>
|
||||
)
|
||||
: <Avatar bgColor="#555" text={displayName} imageSrc={avatarUrl} size="normal" />
|
||||
}
|
||||
<div className="image-pack-profile__content">
|
||||
{
|
||||
isEdit
|
||||
? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input name="nameInput" label="Name" value={displayName} required />
|
||||
<Input name="attributionInput" label="Attribution" value={attribution} resizable />
|
||||
<div>
|
||||
<Button variant="primary" type="submit">Save</Button>
|
||||
<Button onClick={() => setIsEdit(false)}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Text>{displayName}</Text>
|
||||
{onEditProfile && <IconButton size="extra-small" onClick={() => setIsEdit(true)} src={PencilIC} tooltip="Edit" />}
|
||||
</div>
|
||||
{attribution && <Text variant="b3">{attribution}</Text>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="image-pack-profile__usage">
|
||||
<Text variant="b3">Pack usage</Text>
|
||||
<Button
|
||||
onClick={onUsageChange ? handleUsageSelect : undefined}
|
||||
iconSrc={onUsageChange ? ChevronBottomIC : null}
|
||||
>
|
||||
<Text>
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Both'}
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackProfile.defaultProps = {
|
||||
avatarUrl: null,
|
||||
attribution: null,
|
||||
onUsageChange: null,
|
||||
onAvatarChange: null,
|
||||
onEditProfile: null,
|
||||
};
|
||||
ImagePackProfile.propTypes = {
|
||||
avatarUrl: PropTypes.string,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
attribution: PropTypes.string,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onUsageChange: PropTypes.func,
|
||||
onAvatarChange: PropTypes.func,
|
||||
onEditProfile: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ImagePackProfile;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
|
||||
.image-pack-profile {
|
||||
padding: var(--sp-normal);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--sp-tight);
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
|
||||
& > div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-extra-tight);
|
||||
|
||||
& .ic-btn {
|
||||
padding: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
& > form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-extra-tight);
|
||||
& > div:last-child {
|
||||
margin: var(--sp-extra-tight) 0;
|
||||
display: flex;
|
||||
gap: var(--sp-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
&__usage {
|
||||
& > *:first-child {
|
||||
margin-bottom: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackUpload.scss';
|
||||
|
||||
import { scaleDownImage } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
function ImagePackUpload({ onUpload }) {
|
||||
const mx = useMatrixClient();
|
||||
const inputRef = useRef(null);
|
||||
const shortcodeRef = useRef(null);
|
||||
const [imgFile, setImgFile] = useState(null);
|
||||
const [progress, setProgress] = useState(false);
|
||||
|
||||
const handleSubmit = async (evt) => {
|
||||
evt.preventDefault();
|
||||
if (!imgFile) return;
|
||||
const { shortcodeInput } = evt.target;
|
||||
const shortcode = shortcodeInput.value.trim();
|
||||
if (shortcode === '') return;
|
||||
|
||||
setProgress(true);
|
||||
const image = await scaleDownImage(imgFile, 512, 512);
|
||||
const { content_uri: url } = await mx.uploadContent(image);
|
||||
|
||||
onUpload(shortcode, url);
|
||||
setProgress(false);
|
||||
setImgFile(null);
|
||||
shortcodeRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleFileChange = (evt) => {
|
||||
const img = evt.target.files[0];
|
||||
if (!img) return;
|
||||
setImgFile(img);
|
||||
shortcodeRef.current.value = img.name.slice(0, img.name.indexOf('.'));
|
||||
shortcodeRef.current.focus();
|
||||
};
|
||||
const handleRemove = () => {
|
||||
setImgFile(null);
|
||||
inputRef.current.value = null;
|
||||
shortcodeRef.current.value = '';
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="image-pack-upload">
|
||||
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" accept=".png, .gif, .webp" required />
|
||||
{
|
||||
imgFile
|
||||
? (
|
||||
<div className="image-pack-upload__file">
|
||||
<IconButton onClick={handleRemove} src={CirclePlusIC} tooltip="Remove file" />
|
||||
<Text>{imgFile.name}</Text>
|
||||
</div>
|
||||
)
|
||||
: <Button onClick={() => inputRef.current.click()}>Import image</Button>
|
||||
}
|
||||
<Input forwardRef={shortcodeRef} name="shortcodeInput" placeholder="shortcode" required />
|
||||
<Button disabled={progress} variant="primary" type="submit">{progress ? 'Uploading...' : 'Upload'}</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
ImagePackUpload.propTypes = {
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImagePackUpload;
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.image-pack-upload {
|
||||
padding: var(--sp-normal);
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
gap: var(--sp-tight);
|
||||
|
||||
& > .input-container {
|
||||
flex-grow: 1;
|
||||
input {
|
||||
padding: 9px var(--sp-normal);
|
||||
}
|
||||
}
|
||||
&__file {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--bg-surface-low);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
|
||||
& button {
|
||||
--parent-height: 40px;
|
||||
width: var(--parent-height);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& .ic-raw {
|
||||
background-color: var(--bg-caution);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
& .text {
|
||||
@extend .cp-txt__ellipsis;
|
||||
@include dir.side(margin, var(--sp-ultra-tight), var(--sp-normal));
|
||||
max-width: 86px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
|
||||
|
||||
function ImagePackUsageSelector({ usage, onSelect }) {
|
||||
return (
|
||||
<div>
|
||||
<MenuHeader>Usage</MenuHeader>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'emoticon' ? CheckIC : undefined}
|
||||
variant={usage === 'emoticon' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('emoticon')}
|
||||
>
|
||||
Emoji
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'sticker' ? CheckIC : undefined}
|
||||
variant={usage === 'sticker' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('sticker')}
|
||||
>
|
||||
Sticker
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'both' ? CheckIC : undefined}
|
||||
variant={usage === 'both' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('both')}
|
||||
>
|
||||
Both
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackUsageSelector.propTypes = {
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImagePackUsageSelector;
|
||||
|
|
@ -1,359 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomAliases.scss';
|
||||
import { EventTimeline } from 'matrix-js-sdk';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import { Debounce } from '../../../util/common';
|
||||
import { isRoomAliasAvailable } from '../../../util/matrixUtil';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Checkbox from '../../atoms/button/Checkbox';
|
||||
import Toggle from '../../atoms/button/Toggle';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
function useValidate(hsString) {
|
||||
const mx = useMatrixClient();
|
||||
const [debounce] = useState(new Debounce());
|
||||
const [validate, setValidate] = useState({ alias: null, status: cons.status.PRE_FLIGHT });
|
||||
|
||||
const setValidateToDefault = () => {
|
||||
setValidate({
|
||||
alias: null,
|
||||
status: cons.status.PRE_FLIGHT,
|
||||
});
|
||||
};
|
||||
|
||||
const checkValueOK = (value) => {
|
||||
if (value.trim() === '') {
|
||||
setValidateToDefault();
|
||||
return false;
|
||||
}
|
||||
if (!value.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||
setValidate({
|
||||
alias: null,
|
||||
status: cons.status.ERROR,
|
||||
msg: 'Invalid character: only letter, numbers and _- are allowed.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleAliasChange = (e) => {
|
||||
const input = e.target;
|
||||
if (validate.status !== cons.status.PRE_FLIGHT) {
|
||||
setValidateToDefault();
|
||||
}
|
||||
if (checkValueOK(input.value) === false) return;
|
||||
|
||||
debounce._(async () => {
|
||||
const { value } = input;
|
||||
const alias = `#${value}:${hsString}`;
|
||||
if (checkValueOK(value) === false) return;
|
||||
|
||||
setValidate({
|
||||
alias,
|
||||
status: cons.status.IN_FLIGHT,
|
||||
msg: `validating ${alias}...`,
|
||||
});
|
||||
|
||||
const isValid = await isRoomAliasAvailable(mx, alias);
|
||||
setValidate(() => {
|
||||
if (e.target.value !== value) {
|
||||
return { alias: null, status: cons.status.PRE_FLIGHT };
|
||||
}
|
||||
return {
|
||||
alias,
|
||||
status: isValid ? cons.status.SUCCESS : cons.status.ERROR,
|
||||
msg: isValid ? `${alias} is available.` : `${alias} is already in use.`,
|
||||
};
|
||||
});
|
||||
}, 600)();
|
||||
};
|
||||
|
||||
return [validate, setValidateToDefault, handleAliasChange];
|
||||
}
|
||||
|
||||
function getAliases(mx, roomId) {
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const main = room.getCanonicalAlias();
|
||||
const published = room.getAltAliases();
|
||||
if (main && !published.includes(main)) published.splice(0, 0, main);
|
||||
|
||||
return {
|
||||
main,
|
||||
published: [...new Set(published)],
|
||||
local: [],
|
||||
};
|
||||
}
|
||||
|
||||
function RoomAliases({ roomId }) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
const userId = mx.getUserId();
|
||||
const hsString = userId.slice(userId.indexOf(':') + 1);
|
||||
|
||||
const isMountedStore = useStore();
|
||||
const [isPublic, setIsPublic] = useState(false);
|
||||
const [isLocalVisible, setIsLocalVisible] = useState(false);
|
||||
const [aliases, setAliases] = useState(getAliases(mx, roomId));
|
||||
const [selectedAlias, setSelectedAlias] = useState(null);
|
||||
const [deleteAlias, setDeleteAlias] = useState(null);
|
||||
const [validate, setValidateToDefault, handleAliasChange] = useValidate(hsString);
|
||||
|
||||
const canPublishAlias = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.maySendStateEvent('m.room.canonical_alias', userId);
|
||||
|
||||
useEffect(() => {
|
||||
isMountedStore.setItem(true)
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let isUnmounted = false;
|
||||
|
||||
const loadLocalAliases = async () => {
|
||||
let local = [];
|
||||
try {
|
||||
const result = await mx.getLocalAliases(roomId);
|
||||
local = result.aliases.filter((alias) => !aliases.published.includes(alias));
|
||||
} catch {
|
||||
local = [];
|
||||
}
|
||||
aliases.local = [...new Set(local.reverse())];
|
||||
|
||||
if (isUnmounted) return;
|
||||
setAliases({ ...aliases });
|
||||
};
|
||||
const loadVisibility = async () => {
|
||||
const result = await mx.getRoomDirectoryVisibility(roomId);
|
||||
if (isUnmounted) return;
|
||||
setIsPublic(result.visibility === 'public');
|
||||
};
|
||||
loadLocalAliases();
|
||||
loadVisibility();
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
};
|
||||
}, [mx, roomId]);
|
||||
|
||||
const toggleDirectoryVisibility = () => {
|
||||
mx.setRoomDirectoryVisibility(roomId, isPublic ? 'private' : 'public');
|
||||
setIsPublic(!isPublic);
|
||||
};
|
||||
|
||||
const handleAliasSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (validate.status === cons.status.ERROR) return;
|
||||
if (!validate.alias) return;
|
||||
|
||||
const { alias } = validate;
|
||||
const aliasInput = e.target.elements['alias-input'];
|
||||
aliasInput.value = '';
|
||||
setValidateToDefault();
|
||||
|
||||
try {
|
||||
aliases.local.push(alias);
|
||||
setAliases({ ...aliases });
|
||||
await mx.createAlias(alias, roomId);
|
||||
} catch {
|
||||
if (isMountedStore.getItem()) {
|
||||
const lIndex = alias.local.indexOf(alias);
|
||||
if (lIndex === -1) return;
|
||||
aliases.local.splice(lIndex, 1);
|
||||
setAliases({ ...aliases });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAliasSelect = (alias) => {
|
||||
setSelectedAlias(alias === selectedAlias ? null : alias);
|
||||
};
|
||||
|
||||
const handlePublishAlias = (alias) => {
|
||||
const { main, published } = aliases;
|
||||
let { local } = aliases;
|
||||
|
||||
if (!published.includes(aliases)) {
|
||||
published.push(alias);
|
||||
local = local.filter((al) => al !== alias);
|
||||
mx.sendStateEvent(roomId, 'm.room.canonical_alias', {
|
||||
alias: main,
|
||||
alt_aliases: published.filter((al) => al !== main),
|
||||
});
|
||||
setAliases({ main, published, local });
|
||||
setSelectedAlias(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnPublishAlias = (alias) => {
|
||||
let { main, published } = aliases;
|
||||
const { local } = aliases;
|
||||
|
||||
if (published.includes(alias) || main === alias) {
|
||||
if (main === alias) main = null;
|
||||
published = published.filter((al) => al !== alias);
|
||||
local.push(alias);
|
||||
mx.sendStateEvent(roomId, 'm.room.canonical_alias', {
|
||||
alias: main,
|
||||
alt_aliases: published.filter((al) => al !== main),
|
||||
});
|
||||
setAliases({ main, published, local });
|
||||
setSelectedAlias(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetMainAlias = (alias) => {
|
||||
let { main, local } = aliases;
|
||||
const { published } = aliases;
|
||||
|
||||
if (main !== alias) {
|
||||
main = alias;
|
||||
if (!published.includes(alias)) published.splice(0, 0, alias);
|
||||
local = local.filter((al) => al !== alias);
|
||||
mx.sendStateEvent(roomId, 'm.room.canonical_alias', {
|
||||
alias: main,
|
||||
alt_aliases: published.filter((al) => al !== main),
|
||||
});
|
||||
setAliases({ main, published, local });
|
||||
setSelectedAlias(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAlias = async (alias) => {
|
||||
try {
|
||||
setDeleteAlias({ alias, status: cons.status.IN_FLIGHT, msg: 'deleting...' });
|
||||
await mx.deleteAlias(alias);
|
||||
let { main, published, local } = aliases;
|
||||
if (published.includes(alias)) {
|
||||
handleUnPublishAlias(alias);
|
||||
if (main === alias) main = null;
|
||||
published = published.filter((al) => al !== alias);
|
||||
}
|
||||
|
||||
local = local.filter((al) => al !== alias);
|
||||
setAliases({ main, published, local });
|
||||
setDeleteAlias(null);
|
||||
setSelectedAlias(null);
|
||||
} catch (err) {
|
||||
setDeleteAlias({ alias, status: cons.status.ERROR, msg: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
const renderAliasBtns = (alias) => {
|
||||
const isPublished = aliases.published.includes(alias);
|
||||
const isMain = aliases.main === alias;
|
||||
if (deleteAlias?.alias === alias) {
|
||||
const isError = deleteAlias.status === cons.status.ERROR;
|
||||
return (
|
||||
<div className="room-aliases__item-btns">
|
||||
<Text variant="b2">
|
||||
<span style={{ color: isError ? 'var(--tc-danger-high' : 'inherit' }}>{deleteAlias.msg}</span>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="room-aliases__item-btns">
|
||||
{canPublishAlias && !isMain && <Button onClick={() => handleSetMainAlias(alias)} variant="primary">Set as Main</Button>}
|
||||
{!isPublished && canPublishAlias && <Button onClick={() => handlePublishAlias(alias)} variant="positive">Publish</Button>}
|
||||
{isPublished && canPublishAlias && <Button onClick={() => handleUnPublishAlias(alias)} variant="caution">Un-Publish</Button>}
|
||||
<Button onClick={() => handleDeleteAlias(alias)} variant="danger">Delete</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAlias = (alias) => {
|
||||
const isActive = selectedAlias === alias;
|
||||
const disabled = !canPublishAlias && aliases.published.includes(alias);
|
||||
const isMain = aliases.main === alias;
|
||||
|
||||
return (
|
||||
<React.Fragment key={`${alias}-wrapper`}>
|
||||
<div className="room-aliases__alias-item" key={alias}>
|
||||
<Checkbox variant="positive" disabled={disabled} isActive={isActive} onToggle={() => handleAliasSelect(alias)} />
|
||||
<Text>
|
||||
{alias}
|
||||
{isMain && <span>Main</span>}
|
||||
</Text>
|
||||
</div>
|
||||
{isActive && renderAliasBtns(alias)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
let inputState = 'normal';
|
||||
if (validate.status === cons.status.ERROR) inputState = 'error';
|
||||
if (validate.status === cons.status.SUCCESS) inputState = 'success';
|
||||
return (
|
||||
<div className="room-aliases">
|
||||
<SettingTile
|
||||
title="Publish to room directory"
|
||||
content={<Text variant="b3">{`Publish this ${room.isSpaceRoom() ? 'space' : 'room'} to the ${hsString}'s public room directory?`}</Text>}
|
||||
options={(
|
||||
<Toggle
|
||||
isActive={isPublic}
|
||||
onToggle={toggleDirectoryVisibility}
|
||||
disabled={!canPublishAlias}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="room-aliases__content">
|
||||
<MenuHeader>Published addresses</MenuHeader>
|
||||
{(aliases.published.length === 0) && <Text className="room-aliases__message">No published addresses</Text>}
|
||||
{(aliases.published.length > 0 && !aliases.main) && <Text className="room-aliases__message">No Main address (select one from below)</Text>}
|
||||
{aliases.published.map(renderAlias)}
|
||||
<Text className="room-aliases__message" variant="b3">
|
||||
{`Published addresses can be used by anyone on any server to join your ${room.isSpaceRoom() ? 'space' : 'room'}. To publish an address, it needs to be set as a local address first.`}
|
||||
</Text>
|
||||
</div>
|
||||
{ isLocalVisible && (
|
||||
<div className="room-aliases__content">
|
||||
<MenuHeader>Local addresses</MenuHeader>
|
||||
{(aliases.local.length === 0) && <Text className="room-aliases__message">No local addresses</Text>}
|
||||
{aliases.local.map(renderAlias)}
|
||||
<Text className="room-aliases__message" variant="b3">
|
||||
{`Set local addresses for this ${room.isSpaceRoom() ? 'space' : 'room'} so users can find this ${room.isSpaceRoom() ? 'space' : 'room'} through your homeserver.`}
|
||||
</Text>
|
||||
|
||||
<Text className="room-aliases__form-label" variant="b2">Add local address</Text>
|
||||
<form className="room-aliases__form" onSubmit={handleAliasSubmit}>
|
||||
<div className="room-aliases__input-wrapper">
|
||||
<Input
|
||||
name="alias-input"
|
||||
state={inputState}
|
||||
onChange={handleAliasChange}
|
||||
placeholder={`my_${room.isSpaceRoom() ? 'space' : 'room'}_address`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button variant="primary" type="submit">Add</Button>
|
||||
</form>
|
||||
<div className="room-aliases__input-status">
|
||||
{validate.status === cons.status.SUCCESS && <Text className="room-aliases__valid" variant="b2">{validate.msg}</Text>}
|
||||
{validate.status === cons.status.ERROR && <Text className="room-aliases__invalid" variant="b2">{validate.msg}</Text>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="room-aliases__content">
|
||||
<Button onClick={() => setIsLocalVisible(!isLocalVisible)}>
|
||||
{`${isLocalVisible ? 'Hide' : 'Add / View'} local address`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomAliases.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomAliases;
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.room-aliases {
|
||||
&__message,
|
||||
& .setting-tile {
|
||||
margin: var(--sp-tight) var(--sp-normal);
|
||||
}
|
||||
& .setting-tile {
|
||||
margin-bottom: var(--sp-loose);
|
||||
}
|
||||
|
||||
&__alias-item {
|
||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||
@extend .cp-fx__row--s-c;
|
||||
&.checkbox {
|
||||
@include dir.side(margin, 0 , var(--sp-tight));
|
||||
}
|
||||
& .text {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-txt__ellipsis;
|
||||
color: var(--tc-surface-high);
|
||||
span {
|
||||
margin: 0 var(--sp-extra-tight);
|
||||
padding: 0 var(--sp-ultra-tight);
|
||||
color: var(--bg-surface);
|
||||
background-color: var(--tc-surface-low);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__item-btns {
|
||||
@include dir.side(margin, 48px, 0);
|
||||
& button {
|
||||
padding: var(--sp-ultra-tight) var(--sp-tight);
|
||||
margin-bottom: var(--sp-tight);
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
margin-bottom: var(--sp-normal);
|
||||
|
||||
& .checkbox {
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
min-width: 20px;
|
||||
}
|
||||
& > button {
|
||||
margin: 0 var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
padding: var(--sp-normal);
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
&-label {
|
||||
padding: var(--sp-normal) var(--sp-normal) var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
display: flex;
|
||||
@extend .cp-fx__item-one;
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
|
||||
& .input-container {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
}
|
||||
|
||||
&__input-status {
|
||||
padding: 0 var(--sp-normal);
|
||||
}
|
||||
&__valid {
|
||||
color: var(--tc-positive-high);
|
||||
padding-bottom: var(--sp-normal);
|
||||
}
|
||||
&__invalid {
|
||||
color: var(--tc-danger-high);
|
||||
padding-bottom: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
import React, { useReducer, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomEmojis.scss';
|
||||
import { EventTimeline } from 'matrix-js-sdk';
|
||||
|
||||
import { suffixRename } from '../../../util/common';
|
||||
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import ImagePack from '../image-pack/ImagePack';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { getStateEvent, getStateEvents } from '../../utils/room';
|
||||
|
||||
function useRoomPacks(room) {
|
||||
const mx = useMatrixClient();
|
||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
|
||||
const packEvents = getStateEvents(room, 'im.ponies.room_emotes');
|
||||
const unUsablePacks = [];
|
||||
const usablePacks = packEvents.filter((mEvent) => {
|
||||
if (typeof mEvent.getContent()?.images !== 'object') {
|
||||
unUsablePacks.push(mEvent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleEvent = (event, state, prevEvent) => {
|
||||
if (event.getRoomId() !== room.roomId) return;
|
||||
if (event.getType() !== 'im.ponies.room_emotes') return;
|
||||
if (!prevEvent?.getContent()?.images || !event.getContent().images) {
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
mx.on('RoomState.events', handleEvent);
|
||||
return () => {
|
||||
mx.removeListener('RoomState.events', handleEvent);
|
||||
};
|
||||
}, [room, mx]);
|
||||
|
||||
const isStateKeyAvailable = (key) => !getStateEvent(room, 'im.ponies.room_emotes', key);
|
||||
|
||||
const createPack = async (name) => {
|
||||
const packContent = {
|
||||
pack: { display_name: name },
|
||||
images: {},
|
||||
};
|
||||
let stateKey = '';
|
||||
if (unUsablePacks.length > 0) {
|
||||
const mEvent = unUsablePacks[0];
|
||||
stateKey = mEvent.getStateKey();
|
||||
} else {
|
||||
stateKey = packContent.pack.display_name.replace(/\s/g, '-');
|
||||
if (!isStateKeyAvailable(stateKey)) {
|
||||
stateKey = suffixRename(
|
||||
stateKey,
|
||||
isStateKeyAvailable,
|
||||
);
|
||||
}
|
||||
}
|
||||
await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', packContent, stateKey);
|
||||
};
|
||||
|
||||
const deletePack = async (stateKey) => {
|
||||
await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', {}, stateKey);
|
||||
};
|
||||
|
||||
return {
|
||||
usablePacks,
|
||||
createPack,
|
||||
deletePack,
|
||||
};
|
||||
}
|
||||
|
||||
function RoomEmojis({ roomId }) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const { usablePacks, createPack, deletePack } = useRoomPacks(room);
|
||||
|
||||
const canChange = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.maySendStateEvent('im.ponies.room_emote', mx.getUserId());
|
||||
|
||||
const handlePackCreate = (e) => {
|
||||
e.preventDefault();
|
||||
const { nameInput } = e.target;
|
||||
const name = nameInput.value.trim();
|
||||
if (name === '') return;
|
||||
nameInput.value = '';
|
||||
|
||||
createPack(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="room-emojis">
|
||||
{ canChange && (
|
||||
<div className="room-emojis__add-pack">
|
||||
<MenuHeader>Create Pack</MenuHeader>
|
||||
<form onSubmit={handlePackCreate}>
|
||||
<Input name="nameInput" placeholder="Pack Name" required />
|
||||
<Button variant="primary" type="submit">Create pack</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
usablePacks.length > 0
|
||||
? usablePacks.reverse().map((mEvent) => (
|
||||
<ImagePack
|
||||
key={mEvent.getId()}
|
||||
roomId={roomId}
|
||||
stateKey={mEvent.getStateKey()}
|
||||
handlePackDelete={canChange ? deletePack : undefined}
|
||||
/>
|
||||
)) : (
|
||||
<div className="room-emojis__empty">
|
||||
<Text>No emoji or sticker pack.</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomEmojis.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomEmojis;
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
.room-emojis {
|
||||
.image-pack,
|
||||
.room-emojis__add-pack,
|
||||
.room-emojis__empty {
|
||||
margin: var(--sp-normal) 0;
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
overflow: hidden;
|
||||
|
||||
& > .context-menu__header:first-child {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
&__add-pack {
|
||||
& form {
|
||||
margin: var(--sp-normal);
|
||||
display: flex;
|
||||
gap: var(--sp-normal);
|
||||
& .input-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__empty {
|
||||
padding: var(--sp-extra-loose) var(--sp-normal);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomEncryption.scss';
|
||||
import { EventTimeline } from 'matrix-js-sdk';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Toggle from '../../atoms/button/Toggle';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
|
||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { getStateEvents } from '../../utils/room';
|
||||
|
||||
function RoomEncryption({ roomId }) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
const encryptionEvents = getStateEvents(room, 'm.room.encryption');
|
||||
const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
|
||||
const canEnableEncryption = room.getLiveTimeline().getState(EventTimeline.FORWARDS).maySendStateEvent('m.room.encryption', mx.getUserId());
|
||||
|
||||
const handleEncryptionEnable = async () => {
|
||||
const joinRule = room.getJoinRule();
|
||||
const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.';
|
||||
const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly';
|
||||
|
||||
const isConfirmed1 = (joinRule === 'public')
|
||||
? await confirmDialog('Enable encryption', confirmMsg1, 'Continue', 'caution')
|
||||
: true;
|
||||
if (!isConfirmed1) return;
|
||||
if (await confirmDialog('Enable encryption', confirmMsg2, 'Enable', 'caution')) {
|
||||
setIsEncrypted(true);
|
||||
mx.sendStateEvent(roomId, 'm.room.encryption', {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="room-encryption">
|
||||
<SettingTile
|
||||
title="Enable room encryption"
|
||||
content={(
|
||||
<Text variant="b3">Once enabled, encryption cannot be disabled.</Text>
|
||||
)}
|
||||
options={(
|
||||
<Toggle
|
||||
isActive={isEncrypted}
|
||||
onToggle={handleEncryptionEnable}
|
||||
disabled={isEncrypted || !canEnableEncryption}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomEncryption.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomEncryption;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
.room-encryption {
|
||||
& .setting-tile {
|
||||
margin: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomHistoryVisibility.scss';
|
||||
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RadioButton from '../../atoms/button/RadioButton';
|
||||
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const visibility = {
|
||||
WORLD_READABLE: 'world_readable',
|
||||
SHARED: 'shared',
|
||||
INVITED: 'invited',
|
||||
JOINED: 'joined',
|
||||
};
|
||||
|
||||
const items = [{
|
||||
iconSrc: null,
|
||||
text: 'Anyone (including guests)',
|
||||
type: visibility.WORLD_READABLE,
|
||||
}, {
|
||||
iconSrc: null,
|
||||
text: 'Members (all messages)',
|
||||
type: visibility.SHARED,
|
||||
}, {
|
||||
iconSrc: null,
|
||||
text: 'Members (messages after invite)',
|
||||
type: visibility.INVITED,
|
||||
}, {
|
||||
iconSrc: null,
|
||||
text: 'Members (messages after join)',
|
||||
type: visibility.JOINED,
|
||||
}];
|
||||
|
||||
|
||||
function useVisibility(roomId) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const [activeType, setActiveType] = useState(room.getHistoryVisibility());
|
||||
useEffect(() => {
|
||||
setActiveType(room.getHistoryVisibility());
|
||||
}, [room]);
|
||||
|
||||
const setVisibility = useCallback((item) => {
|
||||
if (item.type === activeType.type) return;
|
||||
setActiveType(item.type);
|
||||
mx.sendStateEvent(
|
||||
roomId, 'm.room.history_visibility',
|
||||
{
|
||||
history_visibility: item.type,
|
||||
},
|
||||
);
|
||||
}, [mx, activeType, roomId]);
|
||||
|
||||
return [activeType, setVisibility];
|
||||
}
|
||||
|
||||
function RoomHistoryVisibility({ roomId }) {
|
||||
const [activeType, setVisibility] = useVisibility(roomId);
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId();
|
||||
const room = mx.getRoom(roomId);
|
||||
const { currentState } = room;
|
||||
|
||||
const canChange = currentState.maySendStateEvent('m.room.history_visibility', userId);
|
||||
|
||||
return (
|
||||
<div className="room-history-visibility">
|
||||
{
|
||||
items.map((item) => (
|
||||
<MenuItem
|
||||
variant={activeType === item.type ? 'positive' : 'surface'}
|
||||
key={item.type}
|
||||
iconSrc={item.iconSrc}
|
||||
onClick={() => setVisibility(item)}
|
||||
disabled={(!canChange)}
|
||||
>
|
||||
<Text varient="b1">
|
||||
<span>{item.text}</span>
|
||||
<RadioButton isActive={activeType === item.type} />
|
||||
</Text>
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
<Text variant="b3">Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect.</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomHistoryVisibility.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomHistoryVisibility;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.room-history-visibility {
|
||||
& .context-menu__item .text {
|
||||
margin: 0 !important;
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-fx__row--s-c;
|
||||
|
||||
& span:first-child {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-txt__ellipsis;
|
||||
}
|
||||
|
||||
& .radio-btn {
|
||||
@include dir.side(margin, var(--sp-tight), 0);
|
||||
}
|
||||
}
|
||||
|
||||
& > .text {
|
||||
margin: var(--sp-normal);
|
||||
margin-top: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomIntro.scss';
|
||||
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
|
||||
function RoomIntro({
|
||||
roomId, avatarSrc, name, heading, desc, time,
|
||||
}) {
|
||||
return (
|
||||
<div className="room-intro">
|
||||
<Avatar imageSrc={avatarSrc} text={name} bgColor={colorMXID(roomId)} size="large" />
|
||||
<div className="room-intro__content">
|
||||
<Text className="room-intro__name" variant="h1" weight="medium" primary>{heading}</Text>
|
||||
<Text className="room-intro__desc" variant="b1">{desc}</Text>
|
||||
{ time !== null && <Text className="room-intro__time" variant="b3">{time}</Text>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomIntro.defaultProps = {
|
||||
avatarSrc: null,
|
||||
time: null,
|
||||
};
|
||||
|
||||
RoomIntro.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
avatarSrc: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
name: PropTypes.string.isRequired,
|
||||
heading: PropTypes.node.isRequired,
|
||||
desc: PropTypes.node.isRequired,
|
||||
time: PropTypes.node,
|
||||
};
|
||||
|
||||
export default RoomIntro;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
@use '../../partials/dir';
|
||||
|
||||
.room-intro {
|
||||
margin-top: calc(2 * var(--sp-extra-loose));
|
||||
margin-bottom: var(--sp-extra-loose);
|
||||
--left-pad: calc(var(--sp-normal) + var(--av-small) + var(--sp-tight));
|
||||
@include dir.side(padding, var(--left-pad), var(--sp-extra-tight));
|
||||
|
||||
.room-intro__content {
|
||||
margin-top: var(--sp-extra-loose);
|
||||
width: calc(100% - 88px);
|
||||
}
|
||||
&__name {
|
||||
color: var(--tc-surface-high);
|
||||
}
|
||||
&__desc {
|
||||
color: var(--tc-surface-normal);
|
||||
margin: var(--sp-tight) 0 var(--sp-extra-tight);
|
||||
& a {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
&__time {
|
||||
color: var(--tc-surface-low);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
import React, {
|
||||
useState, useEffect, useCallback,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomMembers.scss';
|
||||
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { openProfileViewer } from '../../../client/action/navigation';
|
||||
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
|
||||
import AsyncSearch from '../../../util/AsyncSearch';
|
||||
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
|
||||
import PeopleSelector from '../people-selector/PeopleSelector';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const PER_PAGE_MEMBER = 50;
|
||||
|
||||
function normalizeMembers(mx, members) {
|
||||
return members.map((member) => ({
|
||||
userId: member.userId,
|
||||
name: getUsernameOfRoomMember(member),
|
||||
username: member.userId.slice(1, member.userId.indexOf(':')),
|
||||
avatarSrc: member.getAvatarUrl(mx.baseUrl, 24, 24, 'crop'),
|
||||
peopleRole: getPowerLabel(member.powerLevel),
|
||||
powerLevel: members.powerLevel,
|
||||
}));
|
||||
}
|
||||
|
||||
function useMemberOfMembership(roomId, membership) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
const [members, setMembers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
let isLoadingMembers = false;
|
||||
|
||||
const updateMemberList = (event) => {
|
||||
if (isLoadingMembers) return;
|
||||
if (event && event?.getRoomId() !== roomId) return;
|
||||
const memberOfMembership = normalizeMembers(
|
||||
mx,
|
||||
room.getMembersWithMembership(membership)
|
||||
.sort(memberByAtoZ).sort(memberByPowerLevel),
|
||||
);
|
||||
setMembers(memberOfMembership);
|
||||
};
|
||||
|
||||
updateMemberList();
|
||||
isLoadingMembers = true;
|
||||
room.loadMembersIfNeeded().then(() => {
|
||||
isLoadingMembers = false;
|
||||
if (!isMounted) return;
|
||||
updateMemberList();
|
||||
});
|
||||
|
||||
mx.on('RoomMember.membership', updateMemberList);
|
||||
mx.on('RoomMember.powerLevel', updateMemberList);
|
||||
return () => {
|
||||
isMounted = false;
|
||||
mx.removeListener('RoomMember.membership', updateMemberList);
|
||||
mx.removeListener('RoomMember.powerLevel', updateMemberList);
|
||||
};
|
||||
}, [mx, membership]);
|
||||
|
||||
return [members];
|
||||
}
|
||||
|
||||
function useSearchMembers(members) {
|
||||
const [searchMembers, setSearchMembers] = useState(null);
|
||||
const [asyncSearch] = useState(new AsyncSearch());
|
||||
|
||||
const reSearch = useCallback(() => {
|
||||
if (searchMembers) {
|
||||
asyncSearch.search(searchMembers.term);
|
||||
}
|
||||
}, [searchMembers, asyncSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
asyncSearch.setup(members, {
|
||||
keys: ['name', 'username', 'userId'],
|
||||
limit: PER_PAGE_MEMBER,
|
||||
});
|
||||
reSearch();
|
||||
}, [members, asyncSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSearchData = (data, term) => setSearchMembers({ data, term });
|
||||
asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchData);
|
||||
return () => {
|
||||
asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchData);
|
||||
};
|
||||
}, [asyncSearch]);
|
||||
|
||||
const handleSearch = (e) => {
|
||||
const term = e.target.value;
|
||||
if (term === '' || term === undefined) {
|
||||
setSearchMembers(null);
|
||||
} else asyncSearch.search(term);
|
||||
};
|
||||
|
||||
return [searchMembers, handleSearch];
|
||||
}
|
||||
|
||||
function RoomMembers({ roomId }) {
|
||||
const [itemCount, setItemCount] = useState(PER_PAGE_MEMBER);
|
||||
const [membership, setMembership] = useState('join');
|
||||
const [members] = useMemberOfMembership(roomId, membership);
|
||||
const [searchMembers, handleSearch] = useSearchMembers(members);
|
||||
|
||||
useEffect(() => {
|
||||
setItemCount(PER_PAGE_MEMBER);
|
||||
}, [searchMembers]);
|
||||
|
||||
const loadMorePeople = () => {
|
||||
setItemCount(itemCount + PER_PAGE_MEMBER);
|
||||
};
|
||||
|
||||
const mList = searchMembers ? searchMembers.data : members.slice(0, itemCount);
|
||||
return (
|
||||
<div className="room-members">
|
||||
<MenuHeader>Search member</MenuHeader>
|
||||
<Input
|
||||
onChange={handleSearch}
|
||||
placeholder="Search for name"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="room-members__header">
|
||||
<MenuHeader>{`${searchMembers ? `Found — ${mList.length}` : members.length} members`}</MenuHeader>
|
||||
<SegmentedControls
|
||||
selected={
|
||||
(() => {
|
||||
const getSegmentIndex = { join: 0, invite: 1, ban: 2 };
|
||||
return getSegmentIndex[membership];
|
||||
})()
|
||||
}
|
||||
segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]}
|
||||
onSelect={(index) => {
|
||||
const memberships = ['join', 'invite', 'ban'];
|
||||
setMembership(memberships[index]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="room-members__list">
|
||||
{mList.map((member) => (
|
||||
<PeopleSelector
|
||||
key={member.userId}
|
||||
onClick={() => openProfileViewer(member.userId, roomId)}
|
||||
avatarSrc={member.avatarSrc}
|
||||
name={member.name}
|
||||
color={colorMXID(member.userId)}
|
||||
peopleRole={member.peopleRole}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
(searchMembers?.data.length === 0 || members.length === 0)
|
||||
&& (
|
||||
<div className="room-members__status">
|
||||
<Text variant="b2">
|
||||
{searchMembers ? `No results found for "${searchMembers.term}"` : 'No members to display'}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
mList.length !== 0
|
||||
&& members.length > itemCount
|
||||
&& searchMembers === null
|
||||
&& <Button onClick={loadMorePeople}>View more</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomMembers.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomMembers;
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-members {
|
||||
& .input-container {
|
||||
margin: var(--sp-normal);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
& .context-menu__header {
|
||||
@extend .cp-fx__item-one;
|
||||
margin-top: 14px;
|
||||
border-top: 1px solid var(--bg-surface-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
& .segmented-controls {
|
||||
@include dir.side(margin, 0, var(--sp-normal));
|
||||
& > button {
|
||||
padding: var(--sp-ultra-tight) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__list {
|
||||
|
||||
& .people-selector__container:last-child {
|
||||
margin-bottom: var(--sp-extra-tight);
|
||||
}
|
||||
& > .btn-surface {
|
||||
width: calc(100% - 32px);
|
||||
margin: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
margin: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomNotification.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RadioButton from '../../atoms/button/RadioButton';
|
||||
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import BellIC from '../../../../public/res/ic/outlined/bell.svg';
|
||||
import BellRingIC from '../../../../public/res/ic/outlined/bell-ring.svg';
|
||||
import BellPingIC from '../../../../public/res/ic/outlined/bell-ping.svg';
|
||||
import BellOffIC from '../../../../public/res/ic/outlined/bell-off.svg';
|
||||
import { getNotificationType } from '../../utils/room';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const items = [
|
||||
{
|
||||
iconSrc: BellIC,
|
||||
text: 'Global',
|
||||
type: cons.notifs.DEFAULT,
|
||||
},
|
||||
{
|
||||
iconSrc: BellRingIC,
|
||||
text: 'All messages',
|
||||
type: cons.notifs.ALL_MESSAGES,
|
||||
},
|
||||
{
|
||||
iconSrc: BellPingIC,
|
||||
text: 'Mentions & Keywords',
|
||||
type: cons.notifs.MENTIONS_AND_KEYWORDS,
|
||||
},
|
||||
{
|
||||
iconSrc: BellOffIC,
|
||||
text: 'Mute',
|
||||
type: cons.notifs.MUTE,
|
||||
},
|
||||
];
|
||||
|
||||
function setRoomNotifType(mx, roomId, newType) {
|
||||
let roomPushRule;
|
||||
try {
|
||||
roomPushRule = mx.getRoomPushRule('global', roomId);
|
||||
} catch {
|
||||
roomPushRule = undefined;
|
||||
}
|
||||
const promises = [];
|
||||
|
||||
if (newType === cons.notifs.MUTE) {
|
||||
if (roomPushRule) {
|
||||
promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id));
|
||||
}
|
||||
promises.push(
|
||||
mx.addPushRule('global', 'override', roomId, {
|
||||
conditions: [
|
||||
{
|
||||
kind: 'event_match',
|
||||
key: 'room_id',
|
||||
pattern: roomId,
|
||||
},
|
||||
],
|
||||
actions: ['dont_notify'],
|
||||
})
|
||||
);
|
||||
return promises;
|
||||
}
|
||||
|
||||
const oldState = getNotificationType(mx, roomId);
|
||||
if (oldState === cons.notifs.MUTE) {
|
||||
promises.push(mx.deletePushRule('global', 'override', roomId));
|
||||
}
|
||||
|
||||
if (newType === cons.notifs.DEFAULT) {
|
||||
if (roomPushRule) {
|
||||
promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id));
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
if (newType === cons.notifs.MENTIONS_AND_KEYWORDS) {
|
||||
promises.push(
|
||||
mx.addPushRule('global', 'room', roomId, {
|
||||
actions: ['dont_notify'],
|
||||
})
|
||||
);
|
||||
promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
// cons.notifs.ALL_MESSAGES
|
||||
promises.push(
|
||||
mx.addPushRule('global', 'room', roomId, {
|
||||
actions: [
|
||||
'notify',
|
||||
{
|
||||
set_tweak: 'sound',
|
||||
value: 'default',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function useNotifications(roomId) {
|
||||
const mx = useMatrixClient();
|
||||
const [activeType, setActiveType] = useState(getNotificationType(mx, roomId));
|
||||
useEffect(() => {
|
||||
setActiveType(getNotificationType(mx, roomId));
|
||||
}, [mx, roomId]);
|
||||
|
||||
const setNotification = useCallback(
|
||||
(item) => {
|
||||
if (item.type === activeType.type) return;
|
||||
setActiveType(item.type);
|
||||
setRoomNotifType(mx, roomId, item.type);
|
||||
},
|
||||
[mx, activeType, roomId]
|
||||
);
|
||||
return [activeType, setNotification];
|
||||
}
|
||||
|
||||
function RoomNotification({ roomId }) {
|
||||
const [activeType, setNotification] = useNotifications(roomId);
|
||||
|
||||
return (
|
||||
<div className="room-notification">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
variant={activeType === item.type ? 'positive' : 'surface'}
|
||||
key={item.type}
|
||||
iconSrc={item.iconSrc}
|
||||
onClick={() => setNotification(item)}
|
||||
>
|
||||
<Text varient="b1">
|
||||
<span>{item.text}</span>
|
||||
<RadioButton isActive={activeType === item.type} />
|
||||
</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomNotification.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomNotification;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.room-notification {
|
||||
& .context-menu__item .text {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-fx__row--s-c;
|
||||
|
||||
& span:first-child {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-txt__ellipsis;
|
||||
}
|
||||
|
||||
& .radio-btn {
|
||||
@include dir.side(margin, var(--sp-tight), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,284 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomPermissions.scss';
|
||||
import { EventTimeline } from 'matrix-js-sdk';
|
||||
|
||||
import { getPowerLabel } from '../../../util/matrixUtil';
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import PowerLevelSelector from '../power-level-selector/PowerLevelSelector';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { getStateEvent } from '../../utils/room';
|
||||
|
||||
const permissionsInfo = {
|
||||
users_default: {
|
||||
name: 'Default role',
|
||||
description: 'Set default role for all members.',
|
||||
default: 0,
|
||||
},
|
||||
events_default: {
|
||||
name: 'Send messages',
|
||||
description: 'Set minimum power level to send messages in room.',
|
||||
default: 0,
|
||||
},
|
||||
'm.reaction': {
|
||||
parent: 'events',
|
||||
name: 'Send reactions',
|
||||
description: 'Set minimum power level to send reactions in room.',
|
||||
default: 0,
|
||||
},
|
||||
redact: {
|
||||
name: 'Delete messages sent by others',
|
||||
description: 'Set minimum power level to delete messages in room.',
|
||||
default: 50,
|
||||
},
|
||||
notifications: {
|
||||
name: 'Ping room',
|
||||
description: 'Set minimum power level to ping room.',
|
||||
default: {
|
||||
room: 50,
|
||||
},
|
||||
},
|
||||
'm.space.child': {
|
||||
parent: 'events',
|
||||
name: 'Manage rooms in space',
|
||||
description: 'Set minimum power level to manage rooms in space.',
|
||||
default: 50,
|
||||
},
|
||||
invite: {
|
||||
name: 'Invite',
|
||||
description: 'Set minimum power level to invite members.',
|
||||
default: 50,
|
||||
},
|
||||
kick: {
|
||||
name: 'Kick',
|
||||
description: 'Set minimum power level to kick members.',
|
||||
default: 50,
|
||||
},
|
||||
ban: {
|
||||
name: 'Ban',
|
||||
description: 'Set minimum power level to ban members.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.avatar': {
|
||||
parent: 'events',
|
||||
name: 'Change avatar',
|
||||
description: 'Set minimum power level to change room/space avatar.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.name': {
|
||||
parent: 'events',
|
||||
name: 'Change name',
|
||||
description: 'Set minimum power level to change room/space name.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.topic': {
|
||||
parent: 'events',
|
||||
name: 'Change topic',
|
||||
description: 'Set minimum power level to change room/space topic.',
|
||||
default: 50,
|
||||
},
|
||||
state_default: {
|
||||
name: 'Change settings',
|
||||
description: 'Set minimum power level to change settings.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.canonical_alias': {
|
||||
parent: 'events',
|
||||
name: 'Change published address',
|
||||
description: 'Set minimum power level to publish and set main address.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.power_levels': {
|
||||
parent: 'events',
|
||||
name: 'Change permissions',
|
||||
description: 'Set minimum power level to change permissions.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.encryption': {
|
||||
parent: 'events',
|
||||
name: 'Enable room encryption',
|
||||
description: 'Set minimum power level to enable room encryption.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.history_visibility': {
|
||||
parent: 'events',
|
||||
name: 'Change history visibility',
|
||||
description: 'Set minimum power level to change room messages history visibility.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.tombstone': {
|
||||
parent: 'events',
|
||||
name: 'Upgrade room',
|
||||
description: 'Set minimum power level to upgrade room.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.pinned_events': {
|
||||
parent: 'events',
|
||||
name: 'Pin messages',
|
||||
description: 'Set minimum power level to pin messages in room.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.server_acl': {
|
||||
parent: 'events',
|
||||
name: 'Change server ACLs',
|
||||
description: 'Set minimum power level to change server ACLs.',
|
||||
default: 50,
|
||||
},
|
||||
'im.vector.modular.widgets': {
|
||||
parent: 'events',
|
||||
name: 'Modify widgets',
|
||||
description: 'Set minimum power level to modify room widgets.',
|
||||
default: 50,
|
||||
},
|
||||
};
|
||||
|
||||
const roomPermsGroups = {
|
||||
'General Permissions': ['users_default', 'events_default', 'm.reaction', 'redact', 'notifications'],
|
||||
'Manage members permissions': ['invite', 'kick', 'ban'],
|
||||
'Room profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
|
||||
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels', 'm.room.encryption', 'm.room.history_visibility'],
|
||||
'Other permissions': ['m.room.tombstone', 'm.room.pinned_events', 'm.room.server_acl', 'im.vector.modular.widgets'],
|
||||
};
|
||||
|
||||
const spacePermsGroups = {
|
||||
'General Permissions': ['users_default', 'm.space.child'],
|
||||
'Manage members permissions': ['invite', 'kick', 'ban'],
|
||||
'Space profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
|
||||
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels'],
|
||||
};
|
||||
|
||||
function useRoomStateUpdate(roomId) {
|
||||
const [, forceUpdate] = useForceUpdate();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useEffect(() => {
|
||||
const handleStateEvent = (event) => {
|
||||
if (event.getRoomId() !== roomId) return;
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
mx.on('RoomState.events', handleStateEvent);
|
||||
return () => {
|
||||
mx.removeListener('RoomState.events', handleStateEvent);
|
||||
};
|
||||
}, [mx, roomId]);
|
||||
}
|
||||
|
||||
function RoomPermissions({ roomId }) {
|
||||
useRoomStateUpdate(roomId);
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
const pLEvent = getStateEvent(room, 'm.room.power_levels');
|
||||
const permissions = pLEvent.getContent();
|
||||
const canChangePermission = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.maySendStateEvent('m.room.power_levels', mx.getUserId());
|
||||
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
|
||||
|
||||
const handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
|
||||
const handlePowerLevelChange = (newPowerLevel) => {
|
||||
if (powerLevel === newPowerLevel) return;
|
||||
|
||||
const newPermissions = { ...permissions };
|
||||
if (parentKey) {
|
||||
newPermissions[parentKey] = {
|
||||
...permissions[parentKey],
|
||||
[permKey]: newPowerLevel,
|
||||
};
|
||||
} else if (permKey === 'notifications') {
|
||||
newPermissions[permKey] = {
|
||||
...permissions[permKey],
|
||||
room: newPowerLevel,
|
||||
};
|
||||
} else {
|
||||
newPermissions[permKey] = newPowerLevel;
|
||||
}
|
||||
|
||||
mx.sendStateEvent(roomId, 'm.room.power_levels', newPermissions);
|
||||
};
|
||||
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(e, '.btn-surface'),
|
||||
(closeMenu) => (
|
||||
<PowerLevelSelector
|
||||
value={powerLevel}
|
||||
max={myPowerLevel}
|
||||
onSelect={(pl) => {
|
||||
closeMenu();
|
||||
handlePowerLevelChange(pl);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const permsGroups = room.isSpaceRoom() ? spacePermsGroups : roomPermsGroups;
|
||||
return (
|
||||
<div className="room-permissions">
|
||||
{
|
||||
Object.keys(permsGroups).map((groupKey) => {
|
||||
const groupedPermKeys = permsGroups[groupKey];
|
||||
return (
|
||||
<div className="room-permissions__card" key={groupKey}>
|
||||
<MenuHeader>{groupKey}</MenuHeader>
|
||||
{
|
||||
groupedPermKeys.map((permKey) => {
|
||||
const permInfo = permissionsInfo[permKey];
|
||||
|
||||
let powerLevel = 0;
|
||||
let permValue = permInfo.parent
|
||||
? permissions[permInfo.parent]?.[permKey]
|
||||
: permissions[permKey];
|
||||
|
||||
if (permValue === undefined) permValue = permInfo.default;
|
||||
|
||||
if (typeof permValue === 'number') {
|
||||
powerLevel = permValue;
|
||||
} else if (permKey === 'notifications') {
|
||||
powerLevel = permValue.room ?? 50;
|
||||
}
|
||||
return (
|
||||
<SettingTile
|
||||
key={permKey}
|
||||
title={permInfo.name}
|
||||
content={<Text variant="b3">{permInfo.description}</Text>}
|
||||
options={(
|
||||
<Button
|
||||
onClick={
|
||||
canChangePermission
|
||||
? (e) => handlePowerSelector(e, permKey, permInfo.parent, powerLevel)
|
||||
: null
|
||||
}
|
||||
iconSrc={canChangePermission ? ChevronBottomIC : null}
|
||||
>
|
||||
<Text variant="b2">
|
||||
{`${getPowerLabel(powerLevel) || 'Member'} - ${powerLevel}`}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomPermissions.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomPermissions;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
.room-permissions {
|
||||
& .setting-tile {
|
||||
margin: 0 var(--sp-normal);
|
||||
margin-top: var(--sp-tight);
|
||||
padding-bottom: var(--sp-tight);
|
||||
border-bottom: 1px solid var(--bg-surface-border);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import Linkify from 'linkify-react';
|
||||
import './RoomProfile.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ImageUpload from '../image-upload/ImageUpload';
|
||||
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
function RoomProfile({ roomId }) {
|
||||
const isMountStore = useStore();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [, forceUpdate] = useForceUpdate();
|
||||
const [status, setStatus] = useState({
|
||||
msg: null,
|
||||
type: cons.status.PRE_FLIGHT,
|
||||
});
|
||||
|
||||
const mx = useMatrixClient();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const isDM = mDirects.has(roomId);
|
||||
let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||
avatarSrc = isDM
|
||||
? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
|
||||
: avatarSrc;
|
||||
const room = mx.getRoom(roomId);
|
||||
const { currentState } = room;
|
||||
const roomName = room.name;
|
||||
const roomTopic = currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
||||
|
||||
const userId = mx.getUserId();
|
||||
|
||||
const canChangeAvatar = currentState.maySendStateEvent('m.room.avatar', userId);
|
||||
const canChangeName = currentState.maySendStateEvent('m.room.name', userId);
|
||||
const canChangeTopic = currentState.maySendStateEvent('m.room.topic', userId);
|
||||
|
||||
useEffect(() => {
|
||||
isMountStore.setItem(true);
|
||||
const handleStateEvent = (mEvent) => {
|
||||
if (mEvent.event.room_id !== roomId) return;
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
mx.on('RoomState.events', handleStateEvent);
|
||||
return () => {
|
||||
mx.removeListener('RoomState.events', handleStateEvent);
|
||||
isMountStore.setItem(false);
|
||||
setStatus({
|
||||
msg: null,
|
||||
type: cons.status.PRE_FLIGHT,
|
||||
});
|
||||
setIsEditing(false);
|
||||
};
|
||||
}, [mx, roomId]);
|
||||
|
||||
const handleOnSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const { target } = e;
|
||||
const roomNameInput = target.elements['room-name'];
|
||||
const roomTopicInput = target.elements['room-topic'];
|
||||
|
||||
try {
|
||||
if (canChangeName) {
|
||||
const newName = roomNameInput.value;
|
||||
if (newName !== roomName && roomName.trim() !== '') {
|
||||
setStatus({
|
||||
msg: 'Saving room name...',
|
||||
type: cons.status.IN_FLIGHT,
|
||||
});
|
||||
await mx.setRoomName(roomId, newName);
|
||||
}
|
||||
}
|
||||
if (canChangeTopic) {
|
||||
const newTopic = roomTopicInput.value;
|
||||
if (newTopic !== roomTopic) {
|
||||
if (isMountStore.getItem()) {
|
||||
setStatus({
|
||||
msg: 'Saving room topic...',
|
||||
type: cons.status.IN_FLIGHT,
|
||||
});
|
||||
}
|
||||
await mx.setRoomTopic(roomId, newTopic);
|
||||
}
|
||||
}
|
||||
if (!isMountStore.getItem()) return;
|
||||
setStatus({
|
||||
msg: 'Saved successfully',
|
||||
type: cons.status.SUCCESS,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!isMountStore.getItem()) return;
|
||||
setStatus({
|
||||
msg: err.message || 'Unable to save.',
|
||||
type: cons.status.ERROR,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEditing = () => {
|
||||
setStatus({
|
||||
msg: null,
|
||||
type: cons.status.PRE_FLIGHT,
|
||||
});
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (url) => {
|
||||
if (url === null) {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Remove avatar',
|
||||
'Are you sure that you want to remove room avatar?',
|
||||
'Remove',
|
||||
'caution'
|
||||
);
|
||||
if (isConfirmed) {
|
||||
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||
}
|
||||
} else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||
};
|
||||
|
||||
const renderEditNameAndTopic = () => (
|
||||
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
|
||||
{canChangeName && (
|
||||
<Input
|
||||
value={roomName}
|
||||
name="room-name"
|
||||
disabled={status.type === cons.status.IN_FLIGHT}
|
||||
label="Name"
|
||||
/>
|
||||
)}
|
||||
{canChangeTopic && (
|
||||
<Input
|
||||
value={roomTopic}
|
||||
name="room-topic"
|
||||
disabled={status.type === cons.status.IN_FLIGHT}
|
||||
minHeight={100}
|
||||
resizable
|
||||
label="Topic"
|
||||
/>
|
||||
)}
|
||||
{(!canChangeName || !canChangeTopic) && (
|
||||
<Text variant="b3">{`You have permission to change ${
|
||||
room.isSpaceRoom() ? 'space' : 'room'
|
||||
} ${canChangeName ? 'name' : 'topic'} only.`}</Text>
|
||||
)}
|
||||
{status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
|
||||
{status.type === cons.status.SUCCESS && (
|
||||
<Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">
|
||||
{status.msg}
|
||||
</Text>
|
||||
)}
|
||||
{status.type === cons.status.ERROR && (
|
||||
<Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">
|
||||
{status.msg}
|
||||
</Text>
|
||||
)}
|
||||
{status.type !== cons.status.IN_FLIGHT && (
|
||||
<div>
|
||||
<Button type="submit" variant="primary">
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={handleCancelEditing}>Cancel</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
|
||||
const renderNameAndTopic = () => (
|
||||
<div
|
||||
className="room-profile__display"
|
||||
style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}
|
||||
>
|
||||
<div>
|
||||
<Text variant="h2" weight="medium" primary>
|
||||
{roomName}
|
||||
</Text>
|
||||
{(canChangeName || canChangeTopic) && (
|
||||
<IconButton
|
||||
src={PencilIC}
|
||||
size="extra-small"
|
||||
tooltip="Edit"
|
||||
onClick={() => setIsEditing(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Text variant="b3">{room.getCanonicalAlias() || room.roomId}</Text>
|
||||
{roomTopic && (
|
||||
<Text variant="b2">
|
||||
<Linkify options={LINKIFY_OPTS}>{roomTopic}</Linkify>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="room-profile">
|
||||
<div className="room-profile__content">
|
||||
{!canChangeAvatar && (
|
||||
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />
|
||||
)}
|
||||
{canChangeAvatar && (
|
||||
<ImageUpload
|
||||
text={roomName}
|
||||
bgColor={colorMXID(roomId)}
|
||||
imageSrc={avatarSrc}
|
||||
onUpload={handleAvatarUpload}
|
||||
onRequestRemove={() => handleAvatarUpload(null)}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && renderNameAndTopic()}
|
||||
{isEditing && renderEditNameAndTopic()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomProfile.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomProfile;
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-profile {
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__row;
|
||||
& .avatar-container {
|
||||
min-width: var(--av-large);
|
||||
}
|
||||
}
|
||||
|
||||
&__display {
|
||||
align-self: flex-end;
|
||||
@include dir.side(margin, var(--sp-loose), 0);
|
||||
|
||||
& > div:first-child {
|
||||
@extend .cp-fx__row--s-c;
|
||||
& > .text {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
}
|
||||
}
|
||||
|
||||
& > *:not(:first-child) {
|
||||
margin-top: var(--sp-ultra-tight);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-form {
|
||||
@extend .cp-fx__item-one;
|
||||
@include dir.side(margin, var(--sp-loose), 0);
|
||||
|
||||
& .input-container {
|
||||
margin-bottom: var(--sp-extra-tight);
|
||||
}
|
||||
|
||||
& > .text {
|
||||
margin-bottom: var(--sp-tight);
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-fx__row;
|
||||
margin-top: var(--sp-tight);
|
||||
|
||||
.btn-primary {
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomVisibility.scss';
|
||||
import { EventTimeline } from 'matrix-js-sdk';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RadioButton from '../../atoms/button/RadioButton';
|
||||
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import HashIC from '../../../../public/res/ic/outlined/hash.svg';
|
||||
import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg';
|
||||
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
|
||||
import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
|
||||
import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
|
||||
import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const visibility = {
|
||||
INVITE: 'invite',
|
||||
RESTRICTED: 'restricted',
|
||||
PUBLIC: 'public',
|
||||
};
|
||||
|
||||
function setJoinRule(mx, roomId, type) {
|
||||
let allow;
|
||||
if (type === visibility.RESTRICTED) {
|
||||
const { currentState } = mx.getRoom(roomId);
|
||||
const mEvent = currentState.getStateEvents('m.space.parent')[0];
|
||||
if (!mEvent) return Promise.resolve(undefined);
|
||||
|
||||
allow = [{
|
||||
room_id: mEvent.getStateKey(),
|
||||
type: 'm.room_membership',
|
||||
}];
|
||||
}
|
||||
|
||||
return mx.sendStateEvent(
|
||||
roomId,
|
||||
'm.room.join_rules',
|
||||
{
|
||||
join_rule: type,
|
||||
allow,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function useVisibility(roomId) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const [activeType, setActiveType] = useState(room.getJoinRule());
|
||||
useEffect(() => {
|
||||
setActiveType(room.getJoinRule());
|
||||
}, [room]);
|
||||
|
||||
const setNotification = useCallback((item) => {
|
||||
if (item.type === activeType.type) return;
|
||||
setActiveType(item.type);
|
||||
setJoinRule(mx, roomId, item.type);
|
||||
}, [mx, activeType, roomId]);
|
||||
|
||||
return [activeType, setNotification];
|
||||
}
|
||||
|
||||
function RoomVisibility({ roomId }) {
|
||||
const [activeType, setVisibility] = useVisibility(roomId);
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
const isSpace = room.isSpaceRoom();
|
||||
const { currentState } = room;
|
||||
|
||||
const noSpaceParent = currentState.getStateEvents('m.space.parent').length === 0;
|
||||
const mCreate = currentState.getStateEvents('m.room.create')[0]?.getContent();
|
||||
const roomVersion = Number(mCreate?.room_version ?? 0);
|
||||
|
||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||
const canChange = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||
|
||||
const items = [{
|
||||
iconSrc: isSpace ? SpaceLockIC : HashLockIC,
|
||||
text: 'Private (invite only)',
|
||||
type: visibility.INVITE,
|
||||
unsupported: false,
|
||||
}, {
|
||||
iconSrc: isSpace ? SpaceIC : HashIC,
|
||||
text: roomVersion < 8 ? 'Restricted (unsupported: required room upgrade)' : 'Restricted (space member can join)',
|
||||
type: visibility.RESTRICTED,
|
||||
unsupported: roomVersion < 8 || noSpaceParent,
|
||||
}, {
|
||||
iconSrc: isSpace ? SpaceGlobeIC : HashGlobeIC,
|
||||
text: 'Public (anyone can join)',
|
||||
type: visibility.PUBLIC,
|
||||
unsupported: false,
|
||||
}];
|
||||
|
||||
return (
|
||||
<div className="room-visibility">
|
||||
{
|
||||
items.map((item) => (
|
||||
<MenuItem
|
||||
variant={activeType === item.type ? 'positive' : 'surface'}
|
||||
key={item.type}
|
||||
iconSrc={item.iconSrc}
|
||||
onClick={() => setVisibility(item)}
|
||||
disabled={(!canChange || item.unsupported)}
|
||||
>
|
||||
<Text varient="b1">
|
||||
<span>{item.text}</span>
|
||||
<RadioButton isActive={activeType === item.type} />
|
||||
</Text>
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomVisibility.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomVisibility;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.room-visibility {
|
||||
& .context-menu__item .text {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-fx__row--s-c;
|
||||
|
||||
& span:first-child {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-txt__ellipsis;
|
||||
}
|
||||
|
||||
& .radio-btn {
|
||||
@include dir.side(margin, var(--sp-tight), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ import cons from '../../../client/state/cons';
|
|||
import navigation from '../../../client/state/navigation';
|
||||
|
||||
import InviteUser from '../invite-user/InviteUser';
|
||||
import SpaceSettings from '../space-settings/SpaceSettings';
|
||||
import RoomSettings from '../room/RoomSettings';
|
||||
|
||||
function Windows() {
|
||||
const [inviteUser, changeInviteUser] = useState({
|
||||
|
|
@ -30,16 +28,12 @@ function Windows() {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InviteUser
|
||||
isOpen={inviteUser.isOpen}
|
||||
roomId={inviteUser.roomId}
|
||||
searchTerm={inviteUser.searchTerm}
|
||||
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
|
||||
/>
|
||||
<SpaceSettings />
|
||||
<RoomSettings />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,200 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomSettings.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Tabs from '../../atoms/tabs/Tabs';
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import RoomProfile from '../../molecules/room-profile/RoomProfile';
|
||||
import RoomNotification from '../../molecules/room-notification/RoomNotification';
|
||||
import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
|
||||
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
|
||||
import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomHistoryVisibility';
|
||||
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
|
||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||
import RoomEmojis from '../../molecules/room-emojis/RoomEmojis';
|
||||
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
||||
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
||||
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
import PopupWindow from '../../molecules/popup-window/PopupWindow';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const tabText = {
|
||||
GENERAL: 'General',
|
||||
MEMBERS: 'Members',
|
||||
EMOJIS: 'Emojis',
|
||||
PERMISSIONS: 'Permissions',
|
||||
SECURITY: 'Security',
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
iconSrc: SettingsIC,
|
||||
text: tabText.GENERAL,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: UserIC,
|
||||
text: tabText.MEMBERS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: EmojiIC,
|
||||
text: tabText.EMOJIS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: ShieldUserIC,
|
||||
text: tabText.PERMISSIONS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: LockIC,
|
||||
text: tabText.SECURITY,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
function GeneralSettings({ roomId }) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Options</MenuHeader>
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Leave room',
|
||||
`Are you sure that you want to leave "${room.name}" room?`,
|
||||
'Leave',
|
||||
'danger'
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
mx.leave(roomId);
|
||||
}}
|
||||
iconSrc={LeaveArrowIC}
|
||||
>
|
||||
Leave
|
||||
</MenuItem>
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Notification (Changing this will only affect you)</MenuHeader>
|
||||
<RoomNotification roomId={roomId} />
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Room visibility (who can join)</MenuHeader>
|
||||
<RoomVisibility roomId={roomId} />
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Room addresses</MenuHeader>
|
||||
<RoomAliases roomId={roomId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
GeneralSettings.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function SecuritySettings({ roomId }) {
|
||||
return (
|
||||
<>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Encryption</MenuHeader>
|
||||
<RoomEncryption roomId={roomId} />
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Message history visibility</MenuHeader>
|
||||
<RoomHistoryVisibility roomId={roomId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
SecuritySettings.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function useWindowToggle(setSelectedTab) {
|
||||
const [window, setWindow] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const openRoomSettings = (roomId, tab) => {
|
||||
setWindow({ roomId, tabText });
|
||||
const tabItem = tabItems.find((item) => item.text === tab);
|
||||
if (tabItem) setSelectedTab(tabItem);
|
||||
};
|
||||
navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, openRoomSettings);
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.ROOM_SETTINGS_TOGGLED, openRoomSettings);
|
||||
};
|
||||
}, [setSelectedTab]);
|
||||
|
||||
const requestClose = () => setWindow(null);
|
||||
|
||||
return [window, requestClose];
|
||||
}
|
||||
|
||||
function RoomSettings() {
|
||||
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
|
||||
const [window, requestClose] = useWindowToggle(setSelectedTab);
|
||||
const isOpen = window !== null;
|
||||
const roomId = window?.roomId;
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const handleTabChange = (tabItem) => {
|
||||
setSelectedTab(tabItem);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
isOpen={isOpen}
|
||||
className="room-settings"
|
||||
title={
|
||||
<Text variant="s1" weight="medium" primary>
|
||||
{isOpen && room.name}
|
||||
<span style={{ color: 'var(--tc-surface-low)' }}> — room settings</span>
|
||||
</Text>
|
||||
}
|
||||
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||
onRequestClose={requestClose}
|
||||
>
|
||||
{isOpen && (
|
||||
<div className="room-settings__content">
|
||||
<RoomProfile roomId={roomId} />
|
||||
<Tabs
|
||||
items={tabItems}
|
||||
defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
|
||||
onSelect={handleTabChange}
|
||||
/>
|
||||
<div className="room-settings__cards-wrapper">
|
||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||
{selectedTab.text === tabText.EMOJIS && <RoomEmojis roomId={roomId} />}
|
||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PopupWindow>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoomSettings;
|
||||
export { tabText };
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
@use '../../partials/dir';
|
||||
@use '../../partials/flex';
|
||||
|
||||
.room-settings {
|
||||
& .pw {
|
||||
background-color: var(--bg-surface-low);
|
||||
}
|
||||
|
||||
& .room-profile {
|
||||
padding: var(--sp-loose) var(--sp-extra-loose);
|
||||
}
|
||||
|
||||
& .tabs__content {
|
||||
padding: 0 var(--sp-normal);
|
||||
}
|
||||
|
||||
&__cards-wrapper {
|
||||
padding: 0 var(--sp-normal);
|
||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||
}
|
||||
}
|
||||
|
||||
.room-settings__card {
|
||||
margin: var(--sp-normal) 0;
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
overflow: hidden;
|
||||
|
||||
& > .context-menu__header:first-child {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.room-settings .room-permissions__card,
|
||||
.room-settings .room-search__form,
|
||||
.room-settings .room-search__result-item,
|
||||
.room-settings .room-members {
|
||||
@extend .room-settings__card;
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './SpaceSettings.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Tabs from '../../atoms/tabs/Tabs';
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import PopupWindow from '../../molecules/popup-window/PopupWindow';
|
||||
import RoomProfile from '../../molecules/room-profile/RoomProfile';
|
||||
import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
|
||||
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
|
||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||
import RoomEmojis from '../../molecules/room-emojis/RoomEmojis';
|
||||
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
||||
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const tabText = {
|
||||
GENERAL: 'General',
|
||||
MEMBERS: 'Members',
|
||||
EMOJIS: 'Emojis',
|
||||
PERMISSIONS: 'Permissions',
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
iconSrc: SettingsIC,
|
||||
text: tabText.GENERAL,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: UserIC,
|
||||
text: tabText.MEMBERS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: EmojiIC,
|
||||
text: tabText.EMOJIS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: ShieldUserIC,
|
||||
text: tabText.PERMISSIONS,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
function GeneralSettings({ roomId }) {
|
||||
const mx = useMatrixClient();
|
||||
const roomName = mx.getRoom(roomId)?.name;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Options</MenuHeader>
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Leave space',
|
||||
`Are you sure that you want to leave "${roomName}" space?`,
|
||||
'Leave',
|
||||
'danger'
|
||||
);
|
||||
if (isConfirmed) mx.leave(roomId);
|
||||
}}
|
||||
iconSrc={LeaveArrowIC}
|
||||
>
|
||||
Leave
|
||||
</MenuItem>
|
||||
</div>
|
||||
<div className="space-settings__card">
|
||||
<MenuHeader>Space visibility (who can join)</MenuHeader>
|
||||
<RoomVisibility roomId={roomId} />
|
||||
</div>
|
||||
<div className="space-settings__card">
|
||||
<MenuHeader>Space addresses</MenuHeader>
|
||||
<RoomAliases roomId={roomId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
GeneralSettings.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function useWindowToggle(setSelectedTab) {
|
||||
const [window, setWindow] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const openSpaceSettings = (roomId, tab) => {
|
||||
setWindow({ roomId, tabText });
|
||||
const tabItem = tabItems.find((item) => item.text === tab);
|
||||
if (tabItem) setSelectedTab(tabItem);
|
||||
};
|
||||
navigation.on(cons.events.navigation.SPACE_SETTINGS_OPENED, openSpaceSettings);
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.SPACE_SETTINGS_OPENED, openSpaceSettings);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const requestClose = () => setWindow(null);
|
||||
|
||||
return [window, requestClose];
|
||||
}
|
||||
|
||||
function SpaceSettings() {
|
||||
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
|
||||
const [window, requestClose] = useWindowToggle(setSelectedTab);
|
||||
const isOpen = window !== null;
|
||||
const roomId = window?.roomId;
|
||||
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const handleTabChange = (tabItem) => {
|
||||
setSelectedTab(tabItem);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
isOpen={isOpen}
|
||||
className="space-settings"
|
||||
title={
|
||||
<Text variant="s1" weight="medium" primary>
|
||||
{isOpen && room.name}
|
||||
<span style={{ color: 'var(--tc-surface-low)' }}> — space settings</span>
|
||||
</Text>
|
||||
}
|
||||
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||
onRequestClose={requestClose}
|
||||
>
|
||||
{isOpen && (
|
||||
<div className="space-settings__content">
|
||||
<RoomProfile roomId={roomId} />
|
||||
<Tabs
|
||||
items={tabItems}
|
||||
defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
|
||||
onSelect={handleTabChange}
|
||||
/>
|
||||
<div className="space-settings__cards-wrapper">
|
||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||
{selectedTab.text === tabText.EMOJIS && <RoomEmojis roomId={roomId} />}
|
||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PopupWindow>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpaceSettings;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
@use '../../partials/dir.scss';
|
||||
|
||||
.space-settings {
|
||||
& .pw {
|
||||
background-color: var(--bg-surface-low);
|
||||
}
|
||||
|
||||
& .room-profile {
|
||||
padding: var(--sp-loose) var(--sp-extra-loose);
|
||||
}
|
||||
|
||||
& .tabs__content {
|
||||
padding: 0 var(--sp-normal);
|
||||
}
|
||||
|
||||
&__cards-wrapper {
|
||||
padding: 0 var(--sp-normal);
|
||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||
}
|
||||
}
|
||||
|
||||
.space-settings__card {
|
||||
margin: var(--sp-normal) 0;
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
overflow: hidden;
|
||||
|
||||
& > .context-menu__header:first-child {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.space-settings .room-permissions__card,
|
||||
.space-settings .room-members {
|
||||
@extend .space-settings__card;
|
||||
}
|
||||
|
|
@ -60,6 +60,7 @@ import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification'
|
|||
import { AutoRestoreBackupOnVerification } from '../components/BackupRestore';
|
||||
import { RoomSettingsRenderer } from '../features/room-settings';
|
||||
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
|
||||
import { SpaceSettingsRenderer } from '../features/space-settings';
|
||||
|
||||
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
|
||||
const { hashRouter } = clientConfig;
|
||||
|
|
@ -109,7 +110,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
return null;
|
||||
}}
|
||||
element={
|
||||
<>
|
||||
<AuthRouteThemeManager>
|
||||
<ClientRoot>
|
||||
<ClientInitStorageAtom>
|
||||
<ClientRoomsNotificationPreferences>
|
||||
|
|
@ -125,6 +126,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
<Outlet />
|
||||
</ClientLayout>
|
||||
<RoomSettingsRenderer />
|
||||
<SpaceSettingsRenderer />
|
||||
<ReceiveSelfDeviceVerification />
|
||||
<AutoRestoreBackupOnVerification />
|
||||
</ClientNonUIFeatures>
|
||||
|
|
@ -132,8 +134,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
</ClientRoomsNotificationPreferences>
|
||||
</ClientInitStorageAtom>
|
||||
</ClientRoot>
|
||||
<AuthRouteThemeManager />
|
||||
</>
|
||||
</AuthRouteThemeManager>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { useEffect } from 'react';
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { configClass, varsClass } from 'folds';
|
||||
import { DarkTheme, LightTheme, ThemeKind, useSystemThemeKind, useThemes } from '../hooks/useTheme';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import {
|
||||
DarkTheme,
|
||||
LightTheme,
|
||||
ThemeContextProvider,
|
||||
ThemeKind,
|
||||
useActiveTheme,
|
||||
useSystemThemeKind,
|
||||
} from '../hooks/useTheme';
|
||||
|
||||
export function UnAuthRouteThemeManager() {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
|
|
@ -21,38 +26,15 @@ export function UnAuthRouteThemeManager() {
|
|||
return null;
|
||||
}
|
||||
|
||||
export function AuthRouteThemeManager() {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
const themes = useThemes();
|
||||
const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||
const [themeId] = useSetting(settingsAtom, 'themeId');
|
||||
const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
|
||||
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
||||
export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
||||
const activeTheme = useActiveTheme();
|
||||
|
||||
// apply normal theme if system theme is disabled
|
||||
useEffect(() => {
|
||||
if (!systemTheme) {
|
||||
document.body.className = '';
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
|
||||
document.body.classList.add(...selectedTheme.classNames);
|
||||
}
|
||||
}, [systemTheme, themes, themeId]);
|
||||
document.body.classList.add(...activeTheme.classNames);
|
||||
}, [activeTheme]);
|
||||
|
||||
// apply preferred system theme if system theme is enabled
|
||||
useEffect(() => {
|
||||
if (systemTheme) {
|
||||
document.body.className = '';
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
const selectedTheme =
|
||||
systemThemeKind === ThemeKind.Dark
|
||||
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
|
||||
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
|
||||
|
||||
document.body.classList.add(...selectedTheme.classNames);
|
||||
}
|
||||
}, [systemTheme, systemThemeKind, themes, lightThemeId, darkThemeId]);
|
||||
|
||||
return null;
|
||||
return <ThemeContextProvider value={activeTheme}>{children}</ThemeContextProvider>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||
import { RoomProvider } from '../../../hooks/useRoom';
|
||||
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||
import { useDirectRooms } from './useDirectRooms';
|
||||
|
|
@ -20,7 +20,7 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
return (
|
||||
<RoomProvider key={room.roomId} value={room}>
|
||||
{children}
|
||||
<IsDirectRoomProvider value>{children}</IsDirectRoomProvider>
|
||||
</RoomProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue