update rich profile

authed media
idk
This commit is contained in:
OfficialDakari 2024-09-15 18:20:50 +05:00
parent 51abb11801
commit 269458977a
36 changed files with 3463 additions and 635 deletions

View file

@ -6,16 +6,16 @@ It's content looks like this:
"banner_url": "mxc://example.com/banner"
}
```
When user opens a room, Extera updates `m.room.member` with banner if it was not updated.
When user opens a room, Extera updates `m.room.member` if it was not updated. All fields from `ru.officialdakari.extera_profile` is appended to `m.room.member`, but every key becomes `xyz.extera.$key`
### Getting user banner from m.room.member
Extera adds custom field to `m.room.member` - `ru.officialdakari.extera_banner`.
Extera adds custom field to `m.room.member` - `xyz.extera.banner_url`.
So `m.room.member` content will look like that:
```json
{
"avatar_url": "mxc://officialdakari.ru/slemrxtUERwSCLINehUdKiZk",
"displayname": "OfficialDakari",
"membership": "join",
"ru.officialdakari.extera_banner": "mxc://officialdakari.ru/EXFmaeTEsbQMSHPPfFEaRlLr"
"xyz.extera.banner_url": "mxc://officialdakari.ru/EXFmaeTEsbQMSHPPfFEaRlLr"
}
```

3143
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -90,7 +90,8 @@
"tippy.js": "6.3.7",
"ua-parser-js": "1.0.35",
"url": "0.11.3",
"uuid": "10.0.0"
"uuid": "10.0.0",
"vite-plugin-pwa": "0.20.5"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "0.2.3",

View file

@ -1,33 +1,85 @@
// Establish a cache name
const cacheName = 'MediaCache';
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(cacheName));
event.waitUntil(caches.open(cacheName).then(() => {
console.log('Cache opened');
}).catch(error => {
console.error('Error opening cache:', error);
}));
});
self.addEventListener('fetch', async (event) => {
// Is this a request for an image?
if (['/_matrix/client/v1/media', '/_matrix/client/v3/media', '/_matrix/media'].find(x => event.request.url.includes(x))) {
// Open the cache
event.respondWith(caches.open(cacheName).then((cache) => {
// Go to the cache first
return cache.match(event.request.url).then((cachedResponse) => {
// Return a cached response if we have one
if (cachedResponse) {
return cachedResponse;
function fetchFromIndexedDB() {
return new Promise((resolve, reject) => {
const dbRequest = indexedDB.open("CinnyDB", 1);
dbRequest.onsuccess = function (event) {
const db = event.target.result;
const transaction = db.transaction("tokens", "readonly");
const store = transaction.objectStore("tokens");
const getRequest = store.get(1);
getRequest.onsuccess = function () {
if (getRequest.result) {
resolve(getRequest.result);
} else {
reject(new Error("No data found"));
}
};
// Otherwise, hit the network
return fetch(event.request).then((fetchedResponse) => {
// Add the network response to the cache for later visits
cache.put(event.request, fetchedResponse.clone());
getRequest.onerror = function (error) {
reject(error);
};
};
// Return the network response
return fetchedResponse;
dbRequest.onerror = function (error) {
reject(error);
};
});
}
self.addEventListener('fetch', (event) => {
// Check if the request is for an image
const isMediaRequest = [
'/_matrix/client/v1/media',
'/_matrix/client/v3/media',
'/_matrix/media'
].some(url => event.request.url.includes(url));
console.debug(`SW !!! Got request to ${event.request.url} it is ${isMediaRequest ? 'Media' : 'not media'}`, event.request);
if (isMediaRequest) {
event.respondWith(
fetchFromIndexedDB().then(({ accessToken }) => {
return caches.open(cacheName).then((cache) => {
// Try to get a cached response
return cache.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
console.log('Returning cached response for:', event.request.url);
return cachedResponse;
}
console.debug(`SW !!! Got a media request ${event.request.url} New headers`, headers, accessToken);
// Fetch from network and cache the response
return fetch({
...event.request,
headers: {
...event.request.headers,
Authorization: `Bearer ${accessToken}`
}
}).then((fetchedResponse) => {
if (fetchedResponse && fetchedResponse.ok) {
cache.put(event.request, fetchedResponse.clone());
}
return fetchedResponse;
}).catch(error => {
console.error('Fetch error:', error);
throw error;
});
});
});
});
}));
} else {
return;
}).catch(error => {
console.error('Error fetching from IndexedDB:', error);
throw error;
})
);
}
});

View file

@ -3,41 +3,41 @@ import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { SpecVersions, specVersions } from '../cs-api';
type SpecVersionsLoaderProps = {
baseUrl: string;
fallback?: () => ReactNode;
error?: (err: unknown, retry: () => void, ignore: () => void) => ReactNode;
children: (versions: SpecVersions) => ReactNode;
baseUrl: string;
fallback?: () => ReactNode;
error?: (err: unknown, retry: () => void, ignore: () => void) => ReactNode;
children: (versions: SpecVersions) => ReactNode;
};
export function SpecVersionsLoader({
baseUrl,
fallback,
error,
children,
baseUrl,
fallback,
error,
children,
}: SpecVersionsLoaderProps) {
const [state, load] = useAsyncCallback(
useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
);
const [ignoreError, setIgnoreError] = useState(false);
const [state, load] = useAsyncCallback(
useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
);
const [ignoreError, setIgnoreError] = useState(false);
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
useEffect(() => {
load();
}, [load]);
useEffect(() => {
load();
}, [load]);
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
return fallback?.();
}
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
return fallback?.();
}
if (!ignoreError && state.status === AsyncStatus.Error) {
return error?.(state.error, load, ignoreCallback);
}
if (!ignoreError && state.status === AsyncStatus.Error) {
return error?.(state.error, load, ignoreCallback);
}
return children(
state.status === AsyncStatus.Success
? state.data
: {
versions: [],
}
);
return children(
state.status === AsyncStatus.Success
? state.data
: {
versions: [],
}
);
}

View file

@ -3,9 +3,10 @@ import { BlockType, MarkType } from './types';
export const resetEditor = (editor: React.RefObject<HTMLTextAreaElement>) => {
const e = editor.current;
if (!e) return;
const wasInFocus = document.activeElement === e;
e.value = '';
e.blur();
e.focus();
if (wasInFocus) e.focus();
e.rows = 1;
e.style.height = `auto`;
};

View file

@ -0,0 +1,15 @@
import React, { ReactNode } from 'react';
import { getText } from '../../../lang';
type HiddenContentProps = {
reason?: string;
children: ReactNode | ReactNode[];
};
export default function HiddenContent({ reason, children }: HiddenContentProps) {
return reason
? <details>
<summary><b>{getText('hidden_content', getText(reason))}</b></summary>
{children}
</details>
: children;
}

View file

@ -8,7 +8,6 @@ export const ReplyBend = style({
export const Reply = style({
marginBottom: toRem(1),
minWidth: 0,
maxWidth: '100%',
minHeight: config.lineHeight.T300,
maxHeight: '600px',
width: '100%',
@ -17,7 +16,7 @@ export const Reply = style({
cursor: 'pointer',
},
},
backgroundColor: color.Background.ContainerHover,
backgroundColor: color.Background.Container,
padding: '5px',
borderRadius: config.radii.R300,
borderStyle: 'solid',

View file

@ -121,22 +121,27 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
}),
[mx, room, navigateRoom, navigateSpace]
);
const hideReason = (replyEvent?.getContent()['space.0x1a8510f2.msc3368.tags'] ?? [])[0];
const bodyJSX = body
&& replyEvent
? (replyEvent.getType() === 'm.sticker'
? getText('image.usage.sticker')
: <RenderMessageContent
displayName={replyEvent?.sender?.rawDisplayName || replyEvent?.sender?.userId || getText('generic.unknown')}
msgType={replyEvent?.getContent().msgtype ?? ''}
ts={replyEvent?.getTs()}
edited={false}
getContent={replyEvent?.getContent.bind(replyEvent) as GetContentCallback}
mediaAutoLoad={mediaAutoLoad}
urlPreview={false}
outlineAttachment
hideAttachment
htmlReactParserOptions={htmlReactParserOptions}
/>)
: hideReason
? <i>{getText(hideReason)}</i>
: <RenderMessageContent
displayName={replyEvent?.sender?.rawDisplayName || replyEvent?.sender?.userId || getText('generic.unknown')}
msgType={replyEvent?.getContent().msgtype ?? ''}
ts={replyEvent?.getTs()}
edited={false}
getContent={replyEvent?.getContent.bind(replyEvent) as GetContentCallback}
mediaAutoLoad={mediaAutoLoad}
urlPreview={false}
outlineAttachment
hideAttachment
htmlReactParserOptions={htmlReactParserOptions}
/>)
: fallbackBody;
return (

View file

@ -20,17 +20,6 @@ export const getFileSrcUrl = async (
const decryptedBlob = await decryptFile(encData, mimeType, encInfo);
return URL.createObjectURL(decryptedBlob);
}
if (httpUrl.includes('/_matrix/client/v1/media')) {
// // Authed media
// const res = await fetch(httpUrl, {
// headers: {
// Authorization: `Bearer ${mx?.getAccessToken()}`
// }
// });
// const data = await res.blob();
// return URL.createObjectURL(data);
httpUrl += `${httpUrl.includes('?') ? '&' : '?'}access_token=${mx?.getAccessToken()}`;
}
return httpUrl;
};

View file

@ -7,7 +7,10 @@ import { TUploadContent } from '../../utils/matrix';
import { getFileTypeIcon } from '../../utils/common';
import { getText } from '../../../lang';
import Icon from '@mdi/react';
import { mdiCheck, mdiClose, mdiFile } from '@mdi/js';
import { mdiCheck, mdiClose, mdiEye, mdiEyeOff, mdiFile } from '@mdi/js';
import { openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
import HideReasonSelector from '../../molecules/hide-reason-selector/HideReasonSelector';
type UploadCardRendererProps = {
file: TUploadContent;
@ -35,11 +38,7 @@ export function UploadCardRenderer({
cancelUpload();
onRemove(file);
};
console.log(file);
console.log(isEncrypted);
console.log(onRemove);
const icon = getFileTypeIcon(file?.type);
return (
@ -60,6 +59,7 @@ export function UploadCardRenderer({
<Text size="B300">{getText('btn.retry')}</Text>
</Chip>
)}
<IconButton
onClick={removeUpload}
aria-label={getText('aria.cancel_upload')}
@ -88,7 +88,7 @@ export function UploadCardRenderer({
}
>
<Text size="H6" truncate>
{typeof file?.name === 'string' ? file.name : file.localURL}
{file.name}
</Text>
{upload.status === UploadStatus.Success && (
<Icon style={{ color: color.Success.Main }} path={mdiCheck} size={1} />

View file

@ -36,6 +36,7 @@ import { UserAvatar } from '../../components/user-avatar';
import { getText, translate } from '../../../lang';
import Icon from '@mdi/react';
import { mdiAccount } from '@mdi/js';
import HiddenContent from '../../components/hidden-content/HiddenContent';
type SearchResultGroupProps = {
room: Room;
@ -95,18 +96,22 @@ export function SearchResultGroup({
return <RedactedContent reason={event.unsigned?.redacted_because.content.reason} />;
}
const hideReason = (event.content['space.0x1a8510f2.msc3368.tags'] ?? [])[0];
return (
<RenderMessageContent
displayName={displayName}
msgType={event.content.msgtype ?? ''}
ts={event.origin_server_ts}
getContent={getContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
highlightRegex={highlightRegex}
outlineAttachment
/>
<HiddenContent reason={hideReason}>
<RenderMessageContent
displayName={displayName}
msgType={event.content.msgtype ?? ''}
ts={event.origin_server_ts}
getContent={getContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
highlightRegex={highlightRegex}
outlineAttachment
/>
</HiddenContent>
);
},
[MessageEvent.Reaction]: (event, displayName, getContent) => {

View file

@ -107,12 +107,14 @@ import { ReplyLayout } from '../../components/message';
import { markAsRead } from '../../../client/action/notifications';
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { getText } from '../../../lang';
import { openHiddenRooms } from '../../../client/action/navigation';
import { openHiddenRooms, openReusableContextMenu } from '../../../client/action/navigation';
import { ScreenSize, useScreenSize } from '../../hooks/useScreenSize';
import Icon from '@mdi/react';
import { mdiAt, mdiBell, mdiBellOff, mdiCheckAll, mdiClose, mdiEmoticon, mdiEmoticonOutline, mdiFile, mdiMicrophone, mdiPlusCircle, mdiPlusCircleOutline, mdiSend, mdiSendOutline, mdiSticker, mdiStickerOutline } from '@mdi/js';
import { mdiAt, mdiBell, mdiBellOff, mdiCheckAll, mdiClose, mdiEmoticon, mdiEmoticonOutline, mdiEye, mdiEyeOff, mdiFile, mdiMicrophone, mdiPlusCircle, mdiPlusCircleOutline, mdiSend, mdiSendOutline, mdiSticker, mdiStickerOutline } from '@mdi/js';
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { useVoiceRecorder } from '../../hooks/useVoiceRecorder';
import { getEventCords } from '../../../util/common';
import HideReasonSelector from '../../molecules/hide-reason-selector/HideReasonSelector';
interface RoomInputProps {
fileDropContainerRef: RefObject<HTMLElement>;
@ -136,7 +138,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const [voiceMessages] = useSetting(settingsAtom, 'voiceMessages');
const [msgContent, setMsgContent] = useState<IContent>();
const [hideReason, setHideReason] = useState<string | undefined>(undefined);
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
const [replyMention, setReplyMention] = useState(true);
const [uploadBoard, setUploadBoard] = useState(true);
const [selectedFiles, setSelectedFiles] = useAtom(roomIdToUploadItemsAtomFamily(roomId));
@ -181,7 +185,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
encryptFiles.forEach((ef) => fileItems.push(ef));
} else {
safeFiles.forEach((f) =>
fileItems.push({ file: f, originalFile: f, encInfo: undefined })
fileItems.push({ file: f, originalFile: f, encInfo: undefined, hideReason: undefined })
);
}
setSelectedFiles({
@ -257,6 +261,19 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
}
};
const handleHide = useCallback((evt?: MouseEvent) => {
openReusableContextMenu('bottom', getEventCords(evt as unknown as Event, 'button'), (closeMenu: () => void) => (
<HideReasonSelector
value={hideReason}
onSelect={(r?: string) => {
closeMenu();
setHideReason(r);
console.debug(`Hide reason is now`, r);
}}
/>
));
}, [hideReason, setHideReason]);
const dontHideKeyboard = useCallback((evt?: MouseEvent) => {
if (evt) {
evt.preventDefault();
@ -342,7 +359,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
'm.mentions': {
user_ids,
room
}
},
};
if (replyDraft || !customHtmlEqualsPlainText(formattedBody, body)) {
@ -373,9 +390,14 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
};
content['m.relates_to'].is_falling_back = false;
}
if (hideReason) {
console.log(`Hide reason is`, hideReason);
content['space.0x1a8510f2.msc3368.tags'] = [hideReason];
}
return content;
}, [mx, room, textAreaRef, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, threadRootId]);
}, [mx, room, textAreaRef, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, threadRootId, hideReason]);
const submit = useCallback(async () => {
const content = getContent();
@ -411,7 +433,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
sendTypingStatus(false);
resetEditor(textAreaRef);
setShowStickerButton(true);
setHideReason(undefined);
setMsgContent(undefined);
setMsgDraft('');
}
}, [msgContent]);
@ -454,6 +478,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
markAsRead(roomId);
}, [mx, roomId]);
const handleChange = useCallback(
(nt: string) => {
setMsgDraft(nt);
},
[setMsgDraft]
);
const handleKeyDown: KeyboardEventHandler = useCallback(
(evt) => {
setShowStickerButton(isEmptyEditor(textAreaRef));
@ -550,6 +581,10 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
ta.selectionStart = index + result.length;
};
useEffect(() => {
if (textAreaRef.current) textAreaRef.current.value = msgDraft;
}, [msgDraft]);
return (
<div ref={ref}>
{selectedFiles.length > 0 && (
@ -648,6 +683,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
disabled={ar.isRecording}
placeholder={ar.isRecording ? getText('placeholder.room_input.voice') : getText(canRedact ? 'placeholder.room_input' : 'placeholder.room_input.be_careful')}
onKeyDown={handleKeyDown}
onChange={handleChange}
onKeyUp={handleKeyUp}
onPaste={handlePaste}
top={
@ -711,8 +747,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
<>
{!ar.isRecording && (
<>
<IconButton onMouseDown={dontHideKeyboard} onClick={readReceipt} variant="SurfaceVariant" size="300" radii="300">
<Icon size={1} path={mdiCheckAll} />
<IconButton onMouseDown={dontHideKeyboard} onClick={handleHide} variant="SurfaceVariant" size="300" radii="300">
<Icon size={1} path={hideReason ? mdiEyeOff : mdiEye} />
</IconButton>
<UseStateProvider initial={undefined}>
{(emojiBoardTab: EmojiBoardTab | undefined, setEmojiBoardTab) => (

View file

@ -118,6 +118,7 @@ import { getText, translate } from '../../../lang';
import { mdiCheckAll, mdiChevronDown, mdiCodeBraces, mdiCodeBracesBox, mdiImageEdit, mdiMessageAlert, mdiPencilBox } from '@mdi/js';
import Icon from '@mdi/react';
import { ThreadPreview } from './message/ThreadPreview';
import HiddenContent from '../../components/hidden-content/HiddenContent';
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => (
@ -1084,6 +1085,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, textAreaRef, threadR
const senderId = mEvent.getSender() ?? '';
const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
const hideReason = ((getContent() as any)['space.0x1a8510f2.msc3368.tags'] ?? [])[0];
// Кажется, я начинаю по-тихоньку разбираться в этом коде.
// Вообще кайф если это первый чужой код, в котором я смог разобраться
@ -1163,17 +1165,19 @@ export function RoomTimeline({ room, eventId, roomInputRef, textAreaRef, threadR
{mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<RenderMessageContent
displayName={senderDisplayName}
msgType={mEvent.getContent().msgtype ?? ''}
ts={mEvent.getTs()}
edited={!!editedEvent}
getContent={getContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions}
outlineAttachment={messageLayout === 2}
/>
<HiddenContent reason={hideReason}>
<RenderMessageContent
displayName={senderDisplayName}
msgType={mEvent.getContent().msgtype ?? ''}
ts={mEvent.getTs()}
edited={!!editedEvent}
getContent={getContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions}
outlineAttachment={messageLayout === 2}
/>
</HiddenContent>
)}
</Message>
);
@ -1184,6 +1188,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, textAreaRef, threadR
const hasReactions = reactions && reactions.length > 0;
const { replyEventId } = mEvent;
const highlighted = focusItem?.index === item && focusItem.highlight;
const hideReason = (mEvent.getContent()['space.0x1a8510f2.msc3368.tags'] ?? [])[0];
return (
<Message
@ -1241,59 +1246,61 @@ export function RoomTimeline({ room, eventId, roomInputRef, textAreaRef, threadR
)
}
>
<EncryptedContent mEvent={mEvent}>
{() => {
if (mEvent.isRedacted()) return <RedactedContent />;
if (mEvent.getType() === MessageEvent.Sticker)
return (
<MSticker
content={mEvent.getContent()}
renderImageContent={(props) => (
<ImageContent
{...props}
autoPlay={mediaAutoLoad}
renderImage={(p) => <Image {...p} loading="lazy" />}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
/>
);
if (mEvent.getType() === MessageEvent.RoomMessage) {
const editedEvent = getEditedEvent(mEventId, mEvent, timelineSet);
const getContent = (() =>
editedEvent?.getContent()['m.new_content'] ??
mEvent.getContent()) as GetContentCallback;
<HiddenContent reason={hideReason}>
<EncryptedContent mEvent={mEvent}>
{() => {
if (mEvent.isRedacted()) return <RedactedContent />;
if (mEvent.getType() === MessageEvent.Sticker)
return (
<MSticker
content={mEvent.getContent()}
renderImageContent={(props) => (
<ImageContent
{...props}
autoPlay={mediaAutoLoad}
renderImage={(p) => <Image {...p} loading="lazy" />}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
/>
);
if (mEvent.getType() === MessageEvent.RoomMessage) {
const editedEvent = getEditedEvent(mEventId, mEvent, timelineSet);
const getContent = (() =>
editedEvent?.getContent()['m.new_content'] ??
mEvent.getContent()) as GetContentCallback;
const senderId = mEvent.getSender() ?? '';
const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
return (
<RenderMessageContent
displayName={senderDisplayName}
msgType={mEvent.getContent().msgtype ?? ''}
ts={mEvent.getTs()}
edited={!!editedEvent}
getContent={getContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions}
outlineAttachment={messageLayout === 2}
/>
);
}
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
const senderId = mEvent.getSender() ?? '';
const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
return (
<RenderMessageContent
displayName={senderDisplayName}
msgType={mEvent.getContent().msgtype ?? ''}
ts={mEvent.getTs()}
edited={!!editedEvent}
getContent={getContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions}
outlineAttachment={messageLayout === 2}
/>
);
}
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
return (
<Text>
<MessageNotDecryptedContent />
</Text>
);
return (
<Text>
<MessageNotDecryptedContent />
<MessageUnsupportedContent />
</Text>
);
return (
<Text>
<MessageUnsupportedContent />
</Text>
);
}}
</EncryptedContent>
}}
</EncryptedContent>
</HiddenContent>
</Message>
);
},

View file

@ -259,10 +259,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name: Command.Hide,
description: getText('command.hide.desc'),
exe: async () => {
const hideDataEvent = mx.getAccountData('ru.officialdakari.extera.hidden_chats');
const hideDataEvent = mx.getAccountData('xyz.extera.hidden_chats');
const hidden_chats = hideDataEvent ? hideDataEvent.getContent().hidden_chats : {};
hidden_chats[room.roomId] = true;
mx.setAccountData('ru.officialdakari.extera.hidden_chats', {
mx.setAccountData('xyz.extera.hidden_chats', {
hidden_chats
});
}
@ -271,10 +271,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name: Command.UnHide,
description: getText('command.unhide.desc'),
exe: async () => {
const hideDataEvent = mx.getAccountData('ru.officialdakari.extera.hidden_chats');
const hideDataEvent = mx.getAccountData('xyz.extera.hidden_chats');
const hidden_chats = hideDataEvent ? hideDataEvent.getContent().hidden_chats : {};
hidden_chats[room.roomId] = false;
mx.setAccountData('ru.officialdakari.extera.hidden_chats', {
mx.setAccountData('xyz.extera.hidden_chats', {
hidden_chats
});
}

View file

@ -6,6 +6,7 @@ import { isMembershipChanged } from '../utils/room';
import { useMatrixClient } from './useMatrixClient';
import { mdiAccount, mdiAccountLock, mdiAccountLockOpen, mdiAccountPlus, mdiAccountRemove, mdiArrowRight, mdiAt } from '@mdi/js';
import { getText, translate } from '../../lang';
import cons from '../../client/state/cons';
export type ParsedResult = {
icon: string;
@ -219,10 +220,12 @@ export const useMemberEventParser = (): MemberEventParser => {
};
}
if (content['ru.officialdakari.extera_banner'] !== prevContent['ru.officialdakari.extera_banner']) {
//@ts-ignore
if (content[cons.EXTERA_BANNER_URL] !== prevContent[cons.EXTERA_BANNER_URL]) {
return {
icon: mdiAccount,
body: content['ru.officialdakari.extera_banner'] ? (
//@ts-ignore
body: content[cons.EXTERA_BANNER_URL] ? (
translate(
'membership.banner',
<b>{userName}</b>,

View file

@ -0,0 +1,31 @@
import React from 'react';
import './HideReasonSelector.scss';
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
import { getText } from '../../../lang';
function HideReasonSelector({
value, onSelect,
}) {
const tags = [
'space.0x1a8510f2.msc3368.spoiler',
'space.0x1a8510f2.msc3368.nsfw',
'space.0x1a8510f2.msc3368.health_risk',
'space.0x1a8510f2.msc3368.health_risk.flashing',
'space.0x1a8510f2.msc3368.graphic',
'space.0x1a8510f2.msc3368.hidden'
];
return (
<div className="hide-reason-selector">
{tags.map(
tag => (
<MenuItem variant={value === tag ? 'positive' : 'surface'} onClick={() => onSelect(tag)}>{getText(tag)}</MenuItem>
)
)}
<MenuItem variant={!value ? 'positive' : 'surface'} onClick={() => onSelect(null)}>{getText('hide_reason.none')}</MenuItem>
</div>
);
}
export default HideReasonSelector;

View file

@ -0,0 +1,20 @@
@use '../../partials/flex';
@use '../../partials/dir';
.hide-reason-selector {
& .context-menu__item .text {
margin: 0 !important;
}
& form {
margin: var(--sp-normal);
display: flex;
& input {
@extend .cp-fx__item-one;
@include dir.side(margin, 0, var(--sp-tight));
width: 148px;
padding: 9px var(--sp-tight);
}
}
}

View file

@ -9,42 +9,41 @@ import { getText } from '../../../lang';
import { mdiCheck } from '@mdi/js';
function PowerLevelSelector({
value, max, onSelect,
value, max, onSelect,
}) {
const handleSubmit = (e) => {
const powerLevel = e.target.elements['power-level']?.value;
if (!powerLevel) return;
onSelect(Number(powerLevel));
};
const handleSubmit = (e) => {
const powerLevel = e.target.elements['power-level']?.value;
if (!powerLevel) return;
onSelect(Number(powerLevel));
};
return (
<div className="power-level-selector">
<MenuHeader>Power level selector</MenuHeader>
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(e); }}>
<input
className="input"
defaultValue={value}
type="number"
name="power-level"
placeholder="Power level"
max={max}
autoComplete="off"
required
/>
<IconButton variant="primary" src={mdiCheck} type="submit" />
</form>
{max >= 0 && <MenuHeader>{getText('pl_selector.presets')}</MenuHeader>}
{max >= 100 && <MenuItem variant={value === 100 ? 'positive' : 'surface'} onClick={() => onSelect(100)}>{getText('power_level.admin')}</MenuItem>}
{max >= 50 && <MenuItem variant={value === 50 ? 'positive' : 'surface'} onClick={() => onSelect(50)}>{getText('power_level.mod')}</MenuItem>}
{max >= 0 && <MenuItem variant={value === 0 ? 'positive' : 'surface'} onClick={() => onSelect(0)}>{getText('power_level.member')}</MenuItem>}
</div>
);
return (
<div className="power-level-selector">
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(e); }}>
<input
className="input"
defaultValue={value}
type="number"
name="power-level"
placeholder="Power level"
max={max}
autoComplete="off"
required
/>
<IconButton variant="primary" src={mdiCheck} type="submit" />
</form>
{max >= 0 && <MenuHeader>{getText('pl_selector.presets')}</MenuHeader>}
{max >= 100 && <MenuItem variant={value === 100 ? 'positive' : 'surface'} onClick={() => onSelect(100)}>{getText('power_level.admin')}</MenuItem>}
{max >= 50 && <MenuItem variant={value === 50 ? 'positive' : 'surface'} onClick={() => onSelect(50)}>{getText('power_level.mod')}</MenuItem>}
{max >= 0 && <MenuItem variant={value === 0 ? 'positive' : 'surface'} onClick={() => onSelect(0)}>{getText('power_level.member')}</MenuItem>}
</div>
);
}
PowerLevelSelector.propTypes = {
value: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,
onSelect: PropTypes.func.isRequired,
value: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,
onSelect: PropTypes.func.isRequired,
};
export default PowerLevelSelector;

View file

@ -29,8 +29,7 @@ import Chip from '../../atoms/chip/Chip';
import IconButton from '../../atoms/button/IconButton';
import Input from '../../atoms/input/Input';
import Avatar from '../../atoms/avatar/Avatar';
import Button from '../../atoms/button/Button';
import { color, config, Button as FoldsButton, IconButton as FoldsIconButton, Header, Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
import { color, config, Button, IconButton as FoldsIconButton, Header, Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
import PowerLevelSelector from '../../molecules/power-level-selector/PowerLevelSelector';
import Dialog from '../../molecules/dialog/Dialog';
@ -38,16 +37,15 @@ import Dialog from '../../molecules/dialog/Dialog';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { getDMRoomFor } from '../../utils/matrix';
import { getDMRoomFor, getMxIdLocalPart, getMxIdServer } from '../../utils/matrix';
import { EventTimeline } from 'matrix-js-sdk';
import Banner from './Banner';
import { getText, translate } from '../../../lang';
import { useBackButton } from '../../hooks/useBackButton';
import { VerificationBadge } from '../../components/verification-badge/VerificationBadge';
import { Box } from 'folds';
import { mdiAccountCancelOutline, mdiAccountMinusOutline, mdiChevronDown, mdiChevronRight, mdiClose, mdiShieldOutline } from '@mdi/js';
import { mdiAccountCancelOutline, mdiAccountMinusOutline, mdiAccountPlusOutline, mdiBlockHelper, mdiCheck, mdiChevronDown, mdiChevronRight, mdiClose, mdiMessageOutline, mdiPlusCircleOutline, mdiShieldOutline } from '@mdi/js';
import Icon from '@mdi/react';
import FocusTrap from 'focus-trap-react';
function ModerationTools({ roomId, userId }) {
const mx = initMatrix.matrixClient;
@ -93,13 +91,11 @@ function ModerationTools({ roomId, userId }) {
setOpen(false);
};
// TODO seperate dialog for entering reason
return (
<>
<Overlay open={open} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<Modal variant="Surface" size='300'>
<Modal variant="Surface" size='300' flexHeight>
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
@ -113,7 +109,7 @@ function ModerationTools({ roomId, userId }) {
{
translate(
ban ? 'title.ban' : 'title.kick',
<b>{roomMember.rawDisplayName}</b>
<b>{roomMember?.rawDisplayName ?? userId}</b>
)
}
</Text>
@ -132,40 +128,26 @@ function ModerationTools({ roomId, userId }) {
<Box direction="Column" gap="100">
<Input name="reason" placeholder={getText(ban ? 'label.profile_viewer.ban_reason' : 'label.profile_viewer.kick_reason')} variant="Secondary" autoComplete='off' />
</Box>
<FoldsButton
<Button
type="submit"
variant="Critical"
>
{getText(ban ? 'btn.profile_viewer.ban' : 'btn.profile_viewer.kick')}
</FoldsButton>
</Button>
</Box>
</Modal>
</OverlayCenter>
</Overlay>
<div className="moderation-tools" style={{ borderColor: color.Surface.ContainerLine }}>
{canIKick && (
<FoldsButton onClick={handleKick} variant='Critical' fill='None' before={<Icon size={1} path={mdiAccountMinusOutline} />}>
{getText('btn.profile_viewer.kick')}
</FoldsButton>
)}
{canIBan && (
<FoldsButton onClick={handleBan} variant='Critical' fill='None' before={<Icon size={1} path={mdiAccountCancelOutline} />}>
{getText('btn.profile_viewer.ban')}
</FoldsButton>
)}
{/* {canIKick && (
<form onSubmit={handleKick}>
<Input label={getText('label.profile_viewer.kick_reason')} name="kick-reason" />
<Button type="submit">{getText('btn.profile_viewer.kick')}</Button>
</form>
{canIKick && (
<Button onClick={handleKick} variant='Critical' fill='None' before={<Icon size={1} path={mdiAccountMinusOutline} />}>
{getText('btn.profile_viewer.kick')}
</Button>
)}
{canIBan && (
<form onSubmit={handleBan}>
<Input label={getText('label.profile_viewer.ban_reason')} name="ban-reason" />
<Button type="submit">{getText('btn.profile_viewer.ban')}</Button>
</form>
)} */}
</div>
<Button onClick={handleBan} variant='Critical' fill='None' before={<Icon size={1} path={mdiAccountCancelOutline} />}>
{getText('btn.profile_viewer.ban')}
</Button>
)}
</>
);
}
@ -219,7 +201,7 @@ function SessionInfo({ userId }) {
}
return (
<div className="session-info">
<div className="session-info" style={{ borderColor: color.Surface.ContainerLine }}>
<MenuItem
onClick={() => setIsVisible(!isVisible)}
iconSrc={isVisible ? mdiChevronDown : mdiChevronRight}
@ -239,6 +221,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
const [isCreatingDM, setIsCreatingDM] = useState(false);
const [isIgnoring, setIsIgnoring] = useState(false);
const [isUserIgnored, setIsUserIgnored] = useState(initMatrix.matrixClient.isUserIgnored(userId));
const [isAdmin, setIsAdmin] = useState(false);
const isMountedRef = useRef(true);
const mx = initMatrix.matrixClient;
@ -247,6 +230,10 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
const member = room.getMember(userId);
const isInvitable = member?.membership !== 'join' && member?.membership !== 'ban';
useEffect(() => {
mx.isSynapseAdministrator().then(setIsAdmin);
}, [mx]);
const [isInviting, setIsInviting] = useState(false);
const [isInvited, setIsInvited] = useState(member?.membership === 'invite');
@ -254,6 +241,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
const userPL = room.getMember(userId)?.powerLevel || 0;
const canIKick =
room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
const canIForceJoin = getMxIdServer(userId) === getMxIdServer(mx.getUserId());
const isBanned = member?.membership === 'ban';
@ -326,25 +314,57 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
}
};
const forceJoin = async () => {
const token = mx.getAccessToken();
const baseUrl = mx.getHomeserverUrl();
if (!token) return console.error('no token');
const response = await fetch(`${baseUrl}/_synapse/admin/v1/join/${roomId}`, {
headers: {
Authorization: `Bearer ${token}`
},
method: 'POST',
body: JSON.stringify({
user_id: userId
})
});
if (!response.ok) {
alert(`Failed to force-join`);
}
};
return (
<div className="profile-viewer__buttons">
<Button variant="primary" onClick={openDM} disabled={isCreatingDM}>
{getText(isCreatingDM ? 'profile_footer.dm.creating' : 'btn.profile_footer.dm')}
</Button>
<>
{(isInvitable && canIForceJoin && room.canInvite(mx.getUserId()) && isAdmin) && (
<Button
before={<Icon size={1} path={mdiPlusCircleOutline} />}
variant='Success'
fill='None'
onClick={forceJoin}
>
{getText('btn.profile_footer.force_join')}
</Button>
)}
{!isUserIgnored && (
<Button variant='Primary' fill='None' onClick={openDM} disabled={isCreatingDM} before={<Icon size={1} path={mdiMessageOutline} />}>
{getText(isCreatingDM ? 'profile_footer.dm.creating' : 'btn.profile_footer.dm')}
</Button>
)}
{isBanned && canIKick && (
<Button variant="positive" onClick={() => roomActions.unban(roomId, userId)}>
<Button before={<Icon size={1} path={mdiCheck} />} variant='Success' fill='None' onClick={() => roomActions.unban(roomId, userId)}>
{getText('btn.profile_footer.unban')}
</Button>
)}
{(isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
<Button onClick={toggleInvite} disabled={isInviting}>
<Button variant='Primary' fill='None' before={<Icon size={1} path={mdiAccountPlusOutline} />} onClick={toggleInvite} disabled={isInviting}>
{isInvited
? `${getText(isInviting ? 'btn.profile_footer.disinviting' : 'btn.profile_footer.disinvite')}`
: `${getText(isInviting ? 'btn.profile_footer.inviting' : 'btn.profile_footer.invite')}`}
</Button>
)}
<Button
variant={isUserIgnored ? 'positive' : 'danger'}
before={<Icon size={1} path={isUserIgnored ? mdiCheck : mdiBlockHelper} />}
variant={isUserIgnored ? 'Success' : 'Critical'}
fill='None'
onClick={toggleIgnore}
disabled={isIgnoring}
>
@ -352,7 +372,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
? `${getText(isIgnoring ? 'btn.profile_footer.unignoring' : 'btn.profile_footer.unignore')}`
: `${getText(isIgnoring ? 'btn.profile_footer.ignoring' : 'btn.profile_footer.ignore')}`}
</Button>
</div>
</>
);
}
ProfileFooter.propTypes = {
@ -447,7 +467,7 @@ function ProfileViewer() {
console.log(membershipContent);
if (typeof membershipContent[cons.EXTERA_BANNER_URL] === 'string' && membershipContent[cons.EXTERA_BANNER_URL].startsWith('mxc://')) {
bannerUrl = mx.mxcUrlToHttp(membershipContent[cons.EXTERA_BANNER_URL]);
bannerUrl = mx.mxcUrlToHttp(membershipContent[cons.EXTERA_BANNER_URL], false, false, false, false, true, true);
}
const canChangeRole =
@ -508,7 +528,9 @@ function ProfileViewer() {
<Text variant="b3">{getText('profile_viewer.power_level')}</Text>
<Button
onClick={canChangeRole ? handlePowerSelector : null}
iconSrc={canChangeRole ? mdiChevronDown : null}
fill='Soft'
variant='Secondary'
before={canChangeRole ? <Icon size={1} path={mdiChevronDown} /> : null}
>
{`${getPowerLabel(powerLevel) || getText('generic.pl_member')} - ${powerLevel}`}
</Button>
@ -516,10 +538,12 @@ function ProfileViewer() {
</div>
<Text>{statusMsg}</Text>
<SessionInfo userId={userId} />
<ModerationTools roomId={roomId} userId={userId} />
{userId !== mx.getUserId() && (
<ProfileFooter roomId={roomId} userId={userId} onRequestClose={closeDialog} />
)}
<div class="action-list" style={{ borderColor: color.Surface.ContainerLine }}>
{userId !== mx.getUserId() && (
<ProfileFooter roomId={roomId} userId={userId} onRequestClose={closeDialog} />
)}
<ModerationTools roomId={roomId} userId={userId} />
</div>
</div>
);
};

View file

@ -9,7 +9,7 @@
& .dialog__content-container {
padding-top: var(--sp-normal);
padding-bottom: 89px;
padding-bottom: 50px;
@include dir.side(padding, var(--sp-normal), var(--sp-normal));
}
}
@ -87,6 +87,15 @@
height: 46px;
}
}
}
.action-list {
margin-top: var(--sp-normal);
border-radius: var(--bo-radius);
border-width: 1px;
border-style: solid;
display: flex;
flex-direction: column;
& button {
align-content: start;
@ -94,15 +103,13 @@
width: 100%;
margin: var(--sp-ultra-tight) 0;
}
border-radius: var(--bo-radius);
border-width: 1px;
border-style: solid;
}
.session-info {
box-shadow: var(--bs-surface-border);
border-radius: var(--bo-radius);
border-width: 1px;
border-style: solid;
overflow: hidden;
& .context-menu__item button {

View file

@ -111,7 +111,7 @@ function AppearanceSection() {
const handleSetWallpaper = async () => {
wallpaperInputRef.current?.click();
};
useEffect(() => {
wallpaperDB.getWallpaper().then(setWallpaperURL);
}, [wallpaperDB]);
@ -432,6 +432,13 @@ function ExteraSection() {
)}
content={<Text variant="b3">{getText('settings.smooth_scroll.desc')}</Text>}
/>
<SettingTile
title={getText('settings.msc3382.title')}
options={(
<div style={{ opacity: 0.5 }}><Toggle disabled={true} /></div>
)}
content={<Text variant="b3">{getText('settings.msc3382.desc')}</Text>}
/>
<SettingTile
title={getText('settings.rename_tg_bot.title')}
options={(

View file

@ -140,7 +140,7 @@ export const useOrphanSpaces = (
};
export const isHidden = (mx: MatrixClient, roomId: string) => {
const hideDataEvent = mx.getAccountData('ru.officialdakari.extera.hidden_chats');
const hideDataEvent = mx.getAccountData('xyz.extera.hidden_chats');
const hidden_chats = hideDataEvent ? hideDataEvent.getContent().hidden_chats : {};
return hidden_chats[roomId] ?? false;
};

View file

@ -1,6 +1,5 @@
import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils';
import { Descendant } from 'slate';
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { TListAtom, createListAtom } from '../list';
import { createUploadAtomFamily } from '../upload';
@ -9,40 +8,41 @@ import { TUploadContent } from '../../utils/matrix';
export const roomUploadAtomFamily = createUploadAtomFamily();
export type TUploadItem = {
file: TUploadContent;
originalFile: TUploadContent;
encInfo: EncryptedAttachmentInfo | undefined;
file: TUploadContent;
originalFile: TUploadContent;
encInfo: EncryptedAttachmentInfo | undefined;
hideReason?: string | undefined;
};
export const roomIdToUploadItemsAtomFamily = atomFamily<string, TListAtom<TUploadItem>>(
createListAtom
createListAtom
);
export type RoomIdToMsgAction =
| {
type: 'PUT';
roomId: string;
msg: Descendant[];
| {
type: 'PUT';
roomId: string;
msg: string;
}
| {
type: 'DELETE';
roomId: string;
| {
type: 'DELETE';
roomId: string;
};
const createMsgDraftAtom = () => atom<Descendant[]>([]);
const createMsgDraftAtom = () => atom<string>('');
export type TMsgDraftAtom = ReturnType<typeof createMsgDraftAtom>;
export const roomIdToMsgDraftAtomFamily = atomFamily<string, TMsgDraftAtom>(() =>
createMsgDraftAtom()
createMsgDraftAtom()
);
export type IReplyDraft = {
userId: string;
eventId: string;
body: string;
formattedBody?: string;
userId: string;
eventId: string;
body: string;
formattedBody?: string;
};
const createReplyDraftAtom = () => atom<IReplyDraft | undefined>(undefined);
export type TReplyDraftAtom = ReturnType<typeof createReplyDraftAtom>;
export const roomIdToReplyDraftAtomFamily = atomFamily<string, TReplyDraftAtom>(() =>
createReplyDraftAtom()
createReplyDraftAtom()
);

View file

@ -51,13 +51,13 @@ const defaultSettings: Settings = {
isPeopleDrawer: true,
memberSortFilterIndex: 0,
enterForNewline: false,
enterForNewline: true,
messageLayout: 0,
messageSpacing: '400',
hideMembershipEvents: false,
hideNickAvatarEvents: true,
hideNickAvatarEvents: false,
mediaAutoLoad: true,
urlPreview: true,
urlPreview: false,
encUrlPreview: false,
showHiddenEvents: false,
@ -73,7 +73,7 @@ const defaultSettings: Settings = {
extera_status_message: `Hello! I am using ${cons.name}.`,
extera_wallpaper: null,
pushesEnabled: false,
newDesignInput: false,
newDesignInput: true,
hideEmojiAdvert: false,
replyFallbacks: false,
voiceMessages: true

View file

@ -50,6 +50,7 @@ export function openInviteUser(roomId, searchTerm) {
}
export function openProfileViewer(userId, roomId) {
console.log(`opening profiel viewer !!! ${userId} ${roomId}`);
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_PROFILE_VIEWER,
userId,

View file

@ -125,10 +125,9 @@ async function sendExteraProfile(roomId) {
const membershipContent = membership.getContent();
const exteraProfileEvent = mx.getAccountData('ru.officialdakari.extera_profile');
const exteraProfile = exteraProfileEvent ? exteraProfileEvent.getContent() : {};
if (membershipContent['ru.officialdakari.extera_banner'] == exteraProfile.banner_url) return;
await mx.sendStateEvent(roomId, 'm.room.member', {
...membershipContent,
'ru.officialdakari.extera_banner': exteraProfile.banner_url
...Object.fromEntries(Object.entries(exteraProfile).map(([k, v]) => [`xyz.extera.${k}`, v]))
}, mx.getUserId());
} catch (e) {
console.error(e);

View file

@ -1,7 +1,7 @@
const cons = {
name: 'Extera',
app_id: 'ru.officialdakari.extera',
version: '1.4',
version: '1.5',
secretKey: {
ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id',
@ -9,8 +9,8 @@ const cons = {
BASE_URL: 'cinny_hs_base_url',
},
DEVICE_DISPLAY_NAME: 'Extera Chat',
IN_CINNY_SPACES: 'ru.officialdakari.extera.spaces',
EXTERA_BANNER_URL: 'ru.officialdakari.extera_banner',
IN_CINNY_SPACES: 'xyz.extera.spaces',
EXTERA_BANNER_URL: 'xyz.extera.banner_url',
supportEventTypes: [
'm.room.create',
'm.room.message',

View file

@ -14,51 +14,33 @@ import settings from './client/state/settings';
import App from './app/pages/App';
import getCachedURL from './app/utils/cache';
import { trimTrailingSlash } from './app/utils/common';
document.body.classList.add(configClass, varsClass);
if (navigator.serviceWorker) navigator.serviceWorker.register('/worker.js');
if (navigator.serviceWorker) navigator.serviceWorker.register('/cacher.js');
settings.applyTheme();
if (navigator.serviceWorker) {
const dbRequest = window.indexedDB.open("CinnyDB", 1);
// Register Service Worker
if ('serviceWorker' in navigator) {
const swUrl =
import.meta.env.MODE === 'production'
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
: `/dev-sw.js?dev-sw`;
dbRequest.onupgradeneeded = function (event: any) {
const db = event.target.result;
if (!db.objectStoreNames.contains("tokens")) {
db.createObjectStore("tokens", { keyPath: "id" });
navigator.serviceWorker.register(swUrl);
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data?.type === 'token' && event.data?.responseKey) {
// Get the token for SW.
const token = localStorage.getItem('cinny_access_token') ?? undefined;
event.source!.postMessage({
responseKey: event.data.responseKey,
token,
});
}
};
dbRequest.onsuccess = function (event: any) {
const db = event.target.result;
const transaction = db.transaction("tokens", "readwrite");
const store = transaction.objectStore("tokens");
const data = {
id: 1,
baseUrl: localStorage.cinny_hs_base_url,
accessToken: localStorage.cinny_access_token
};
store.put(data);
transaction.oncomplete = function () {
console.log("Data saved to IndexedDB.");
};
transaction.onerror = function (error: any) {
console.error("Transaction failed: ", error);
};
};
dbRequest.onerror = function (error) {
console.error("Error opening database: ", error);
};
});
}
(window as any).getCachedURL = getCachedURL;
const mountApp = () => {
const rootContainer = document.getElementById('root');

View file

@ -917,5 +917,17 @@
"change_password.title": "New password",
"change_password.old": "Enter old password",
"msg_menu.download": "Download",
"msg_menu.goto": "View"
"msg_menu.goto": "View",
"btn.profile_footer.force_join": "Add to room",
"settings.msc3382.title": "Inline attachments (MSC3382)",
"settings.msc3382.desc": "Send multiple attachments in one message",
"space.0x1a8510f2.msc3368.spoiler": "Spoiler",
"space.0x1a8510f2.msc3368.nsfw": "NSFW",
"space.0x1a8510f2.msc3368.health_risk": "Health risk",
"space.0x1a8510f2.msc3368.health_risk.flashing": "Flash warning",
"space.0x1a8510f2.msc3368.graphic": "Graphic warning",
"space.0x1a8510f2.msc3368.hidden": "Hidden content",
"hide_reason.none": "None",
"aria.set_hide_reason": "Set hide reason",
"hidden_content": "Click to view ({0})"
}

View file

@ -917,5 +917,17 @@
"msg_menu.download": "Скачать",
"msg_menu.goto": "Перейти",
"title.ban": "Забанить {0}",
"title.kick": "Выгнать {0}"
"title.kick": "Выгнать {0}",
"btn.profile_footer.force_join": "Добавить",
"settings.msc3382.title": "Вложения MSC3382",
"settings.msc3382.desc": "Отправлять несколько вложений в одном сообщении",
"space.0x1a8510f2.msc3368.spoiler": "Спойлер",
"space.0x1a8510f2.msc3368.nsfw": "NSFW",
"space.0x1a8510f2.msc3368.health_risk": "Риск для здоровья",
"space.0x1a8510f2.msc3368.health_risk.flashing": "Мерцание",
"space.0x1a8510f2.msc3368.graphic": "Графика",
"space.0x1a8510f2.msc3368.hidden": "Скрытое содержимое",
"hide_reason.none": "Не скрывать",
"aria.set_hide_reason": "Причина скрытия",
"hidden_content": "Нажмите чтобы показать ({0})"
}

68
src/sw.ts Normal file
View file

@ -0,0 +1,68 @@
/// <reference lib="WebWorker" />
const cacheName = 'MediaCache';
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(cacheName).then(() => {
console.log('Cache opened');
}).catch(error => {
console.error('Error opening cache:', error);
}));
});
export type { };
declare const self: ServiceWorkerGlobalScope;
async function askForAccessToken(client: Client): Promise<string | undefined> {
return new Promise((resolve) => {
const responseKey = Math.random().toString(36);
const listener = (event: ExtendableMessageEvent) => {
if (event.data.responseKey !== responseKey) return;
resolve(event.data.token);
self.removeEventListener('message', listener);
};
self.addEventListener('message', listener);
client.postMessage({ responseKey, type: 'token' });
});
}
function fetchConfig(token?: string): RequestInit | undefined {
if (!token) return undefined;
return {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
self.addEventListener('fetch', (event: FetchEvent) => {
const { url, method } = event.request;
if (method !== 'GET') return;
if (
!url.includes('/_matrix/client/v1/media/download') &&
!url.includes('/_matrix/client/v1/media/thumbnail')
) {
return;
}
event.respondWith(
(async (): Promise<Response> => {
const cache = await caches.open(cacheName);
const cachedMedia = await cache.match(url);
if (cachedMedia) {
return cachedMedia;
}
const client = await self.clients.get(event.clientId);
let token: string | undefined;
if (client) token = await askForAccessToken(client);
const res = await fetch(url, fetchConfig(token));
if (res?.ok) {
await cache.put(url, res.clone());
}
return res;
})()
);
});

View file

@ -1,12 +1,13 @@
export enum AccountDataEvent {
PushRules = 'm.push_rules',
Direct = 'm.direct',
IgnoredUserList = 'm.ignored_user_list',
PushRules = 'm.push_rules',
Direct = 'm.direct',
IgnoredUserList = 'm.ignored_user_list',
CinnySpaces = 'ru.officialdakari.extera.spaces',
CinnySpaces = 'xyz.extera.spaces',
ExteraProfile = 'ru.officialdakari.extera_profile',
ElementRecentEmoji = 'io.element.recent_emoji',
ElementRecentEmoji = 'io.element.recent_emoji',
PoniesUserEmotes = 'im.ponies.user_emotes',
PoniesEmoteRooms = 'im.ponies.emote_rooms',
PoniesUserEmotes = 'im.ponies.user_emotes',
PoniesEmoteRooms = 'im.ponies.emote_rooms',
}

View file

@ -12,7 +12,7 @@ export type IMemberContent = {
membership?: Membership;
reason?: string;
is_direct?: boolean;
'ru.officialdakari.extera_banner'?: string;
'xyz.extera.banner_url'?: string;
};
export enum StateEvent {

View file

@ -22,7 +22,7 @@ export function getUsername(userId) {
const mx = initMatrix.matrixClient;
const user = mx.getUser(userId);
if (user === null) return userId;
let username = user.displayName;
let username = user.rawDisplayName;
if (typeof username === 'undefined') {
username = userId;
}
@ -30,7 +30,7 @@ export function getUsername(userId) {
}
export function getUsernameOfRoomMember(roomMember) {
return roomMember.name || roomMember.userId;
return roomMember.rawDisplayName || roomMember.userId;
}
export async function isRoomAliasAvailable(alias) {

View file

@ -8,6 +8,7 @@ import inject from '@rollup/plugin-inject';
import topLevelAwait from 'vite-plugin-top-level-await';
import buildConfig from './build.config';
import { readFileSync } from 'fs';
import { VitePWA } from 'vite-plugin-pwa';
const copyFiles = {
targets: [
@ -72,6 +73,20 @@ export default defineConfig({
vanillaExtractPlugin(),
wasm(),
react(),
VitePWA({
srcDir: 'src',
filename: 'sw.ts',
strategies: 'injectManifest',
injectRegister: false,
manifest: false,
injectManifest: {
injectionPoint: undefined,
},
devOptions: {
enabled: true,
type: 'module'
}
})
],
optimizeDeps: {
esbuildOptions: {