mirror of
https://github.com/officialdakari/Extera.git
synced 2025-04-11 23:08:46 +02:00
update rich profile
authed media idk
This commit is contained in:
parent
51abb11801
commit
269458977a
36 changed files with 3463 additions and 635 deletions
|
@ -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
3143
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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`;
|
||||
};
|
||||
|
|
15
src/app/components/hidden-content/HiddenContent.tsx
Normal file
15
src/app/components/hidden-content/HiddenContent.tsx
Normal 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;
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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={(
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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})"
|
||||
}
|
|
@ -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
68
src/sw.ts
Normal 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;
|
||||
})()
|
||||
);
|
||||
});
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Add table
Reference in a new issue