mirror of
https://github.com/etkecc/synapse-admin.git
synced 2024-11-23 16:25:21 +03:00
Expose user avatar URL field in the UI (#27)
* wip * some fixes * update readme * update readme * Add option to change/erase any user's avatar * Fix README * Remove mutationMode from Edit * remove log * update readme
This commit is contained in:
parent
d5113aad72
commit
24cf0a60bf
13 changed files with 138 additions and 78 deletions
16
.github/CONTRIBUTING.md
vendored
16
.github/CONTRIBUTING.md
vendored
|
@ -1,16 +0,0 @@
|
||||||
# Contributing to [etkecc/synapse-admin](https://github.com/etkecc/synapse-admin)
|
|
||||||
|
|
||||||
While etke.cc fork is intended to accept more QoL changes and features,
|
|
||||||
it's good idea to open PR into the upstream repo: [Awesome-Technologies/Synapse-Admin](https://github.com/Awesome-Technologies/synapse-admin).
|
|
||||||
|
|
||||||
1. Use the etkecc/synapse-admin **master** branch as your branch upstream: `git checkout master; git pull; git checkout -b my-new-feature`
|
|
||||||
2. Once your changes are ready, please, open **2** PRs: one from your branch to `Awesome-Technologies/Synapse-Admin` **master**, and another one to `etkecc/synapse-admin` **main**
|
|
||||||
3. Once PR is accepted in the `etkecc/synapse-admin`, update `README.md` file (either directly in the `main` branch, or via another PR) to add link to the merged PR in the [Fork differences](https://github.com/etkecc/synapse-admin#fork-differences) section
|
|
||||||
|
|
||||||
### Why?
|
|
||||||
|
|
||||||
The upstream project may not want to accept all the changes, so to ensure they are not lost, we will gladly add them to the etke.cc fork.
|
|
||||||
Unfortunately, it's challenging to keep changes separated, so to avoid messing upstream and fork changes (e.g., CI changes that should not be pushed to the upstream, as they intended for this fork specifically), there are 2 branches:
|
|
||||||
|
|
||||||
* `master` - read-only copy of upstream's master branch to easily sync changes, and use it as base for new PRs
|
|
||||||
* `main` - fork-own branch with all changes
|
|
18
README.md
18
README.md
|
@ -1,26 +1,19 @@
|
||||||
[![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
|
# Synapse Admin UI [![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
|
||||||
[![Build Status](https://api.travis-ci.com/Awesome-Technologies/synapse-admin.svg?branch=master)](https://app.travis-ci.com/github/Awesome-Technologies/synapse-admin)
|
|
||||||
[![build-test](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml)
|
|
||||||
[![gh-pages](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/edge_ghpage.yml/badge.svg)](https://awesome-technologies.github.io/synapse-admin/)
|
|
||||||
[![docker-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/docker-release.yml/badge.svg)](https://hub.docker.com/r/awesometechnologies/synapse-admin)
|
|
||||||
[![github-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/github-release.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/releases)
|
|
||||||
|
|
||||||
# Synapse admin ui
|
|
||||||
|
|
||||||
This project is built using [react-admin](https://marmelab.com/react-admin/).
|
This project is built using [react-admin](https://marmelab.com/react-admin/).
|
||||||
|
|
||||||
## Fork differences
|
## Fork differences
|
||||||
|
|
||||||
|
With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
|
||||||
|
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
|
||||||
|
user-friendly interface for managing Synapse homeservers.
|
||||||
|
|
||||||
### Available via CDN
|
### Available via CDN
|
||||||
|
|
||||||
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
|
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
|
|
||||||
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
|
|
||||||
user-friendly interface for managing Synapse homeservers.
|
|
||||||
|
|
||||||
The following changes are already implemented:
|
The following changes are already implemented:
|
||||||
|
|
||||||
* [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
* [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
||||||
|
@ -38,6 +31,7 @@ The following changes are already implemented:
|
||||||
* [Add UI option to block deleted rooms from being rejoined](https://github.com/etkecc/synapse-admin/pull/26)
|
* [Add UI option to block deleted rooms from being rejoined](https://github.com/etkecc/synapse-admin/pull/26)
|
||||||
* [Fix required fields check on Bulk registration CSV upload](https://github.com/etkecc/synapse-admin/pull/32)
|
* [Fix required fields check on Bulk registration CSV upload](https://github.com/etkecc/synapse-admin/pull/32)
|
||||||
* [Fix requests with invalid MXIDs on Bulk registration](https://github.com/etkecc/synapse-admin/pull/33)
|
* [Fix requests with invalid MXIDs on Bulk registration](https://github.com/etkecc/synapse-admin/pull/33)
|
||||||
|
* [Expose user avatar URL field in the UI](https://github.com/etkecc/synapse-admin/pull/27)
|
||||||
|
|
||||||
_the list will be updated as new changes are added_
|
_the list will be updated as new changes are added_
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,7 @@ const de: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Lösche Benutzerdaten",
|
erase: "Lösche Benutzerdaten",
|
||||||
|
erase_avatar: "Avatar löschen"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|
|
@ -146,6 +146,7 @@ const en: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Erase user data",
|
erase: "Erase user data",
|
||||||
|
erase_avatar: "Erase avatar"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|
|
@ -144,6 +144,7 @@ const fr: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Effacer les données de l'utilisateur",
|
erase: "Effacer les données de l'utilisateur",
|
||||||
|
erase_avatar: "Effacer l'avatar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|
1
src/i18n/index.d.ts
vendored
1
src/i18n/index.d.ts
vendored
|
@ -142,6 +142,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||||
};
|
};
|
||||||
action: {
|
action: {
|
||||||
erase: string;
|
erase: string;
|
||||||
|
erase_avatar: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|
|
@ -155,6 +155,7 @@ const ru: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Удалить данные пользователя",
|
erase: "Удалить данные пользователя",
|
||||||
|
erase_avatar: "Удалить аватар",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|
|
@ -139,6 +139,7 @@ const zh: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "抹除用户信息",
|
erase: "抹除用户信息",
|
||||||
|
erase_avatar: "抹掉头像",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|
|
@ -55,6 +55,8 @@ import {
|
||||||
ToolbarClasses,
|
ToolbarClasses,
|
||||||
Identifier,
|
Identifier,
|
||||||
RaRecord,
|
RaRecord,
|
||||||
|
ImageInput,
|
||||||
|
ImageField,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
@ -101,26 +103,24 @@ const userFilters = [
|
||||||
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode, ownUserIsSelected: boolean }> = (props) => {
|
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean }> = props => {
|
||||||
const ownUserIsSelected = props.ownUserIsSelected;
|
const ownUserIsSelected = props.ownUserIsSelected;
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (ownUserIsSelected) {
|
if (ownUserIsSelected) {
|
||||||
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>)
|
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div onClickCapture={handleDeleteClick}>
|
return <div onClickCapture={handleDeleteClick}>{props.children}</div>;
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserBulkActionButtons = () => {
|
const UserBulkActionButtons = () => {
|
||||||
const record = useListContext();
|
const record = useListContext();
|
||||||
const [ ownUserIsSelected, setOwnUserIsSelected ] = useState(false);
|
const [ownUserIsSelected, setOwnUserIsSelected] = useState(false);
|
||||||
const selectedIds = record.selectedIds;
|
const selectedIds = record.selectedIds;
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
|
@ -128,19 +128,20 @@ const UserBulkActionButtons = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
||||||
}, [ selectedIds ]);
|
}, [selectedIds]);
|
||||||
|
|
||||||
|
return (
|
||||||
return <>
|
<>
|
||||||
<ServerNoticeBulkButton />
|
<ServerNoticeBulkButton />
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||||
<BulkDeleteButton
|
<BulkDeleteButton
|
||||||
label="resources.users.action.erase"
|
label="resources.users.action.erase"
|
||||||
confirmTitle="resources.users.helper.erase"
|
confirmTitle="resources.users.helper.erase"
|
||||||
mutationMode="pessimistic"
|
mutationMode="pessimistic"
|
||||||
/>
|
/>
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
|
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
|
||||||
|
@ -204,9 +205,12 @@ const UserEditActions = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserCreate = (props: CreateProps) => (
|
export const UserCreate = (props: CreateProps) => (
|
||||||
<Create { ...props} redirect={(resource, id, data) => {
|
<Create
|
||||||
return `users/${id}`;
|
{...props}
|
||||||
}}>
|
redirect={(resource, id, data) => {
|
||||||
|
return `users/${id}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||||
<TextInput source="displayname" validate={maxLength(256)} />
|
<TextInput source="displayname" validate={maxLength(256)} />
|
||||||
|
@ -237,7 +241,7 @@ const UserTitle = () => {
|
||||||
{translate("resources.users.name", {
|
{translate("resources.users.name", {
|
||||||
smart_count: 1,
|
smart_count: 1,
|
||||||
})}{" "}
|
})}{" "}
|
||||||
{record ? ( record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
|
{record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -250,29 +254,34 @@ const UserEditToolbar = () => {
|
||||||
ownUserIsSelected = record.id === ownUserId;
|
ownUserIsSelected = record.id === ownUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
<div className={ToolbarClasses.defaultToolbar}>
|
<>
|
||||||
<Toolbar sx={{ justifyContent: "space-between" }}>
|
<div className={ToolbarClasses.defaultToolbar}>
|
||||||
|
<Toolbar sx={{ justifyContent: "space-between" }}>
|
||||||
<SaveButton />
|
<SaveButton />
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||||
<DeleteButton />
|
<DeleteButton />
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserBooleanInput = (props) => {
|
const UserBooleanInput = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
const isOwnUser = false;
|
|
||||||
let ownUserIsSelected = false;
|
let ownUserIsSelected = false;
|
||||||
if (record && (record.id === ownUserId)) {
|
if (record && record.id === ownUserId) {
|
||||||
ownUserIsSelected = true;
|
ownUserIsSelected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}><BooleanInput {...props} disabled={ownUserIsSelected} /></UserPreventSelfDelete>
|
return (
|
||||||
}
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||||
|
<BooleanInput {...props} disabled={ownUserIsSelected} />
|
||||||
|
</UserPreventSelfDelete>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const UserEdit = (props: EditProps) => {
|
export const UserEdit = (props: EditProps) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
@ -281,7 +290,11 @@ export const UserEdit = (props: EditProps) => {
|
||||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
||||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
||||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
|
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px" }} />
|
||||||
|
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
|
||||||
|
<ImageInput source="avatar_file" label="resources.users.fields.avatar" accept="image/*">
|
||||||
|
<ImageField source="src" title="Avatar" />
|
||||||
|
</ImageInput>
|
||||||
<TextInput source="id" disabled />
|
<TextInput source="id" disabled />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" />
|
||||||
<PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
<PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
||||||
|
|
|
@ -101,7 +101,7 @@ describe("authProvider", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject if error.status is 401", async () => {
|
it("should reject if error.status is 401", async () => {
|
||||||
await expect(authProvider.checkError({ status: 401 })).rejects.toBeUndefined();
|
await expect(authProvider.checkError(new HttpError("test-error", 401, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject if error.status is 403", async () => {
|
it("should reject if error.status is 403", async () => {
|
||||||
|
|
|
@ -18,7 +18,7 @@ describe("dataProvider", () => {
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
users: [
|
users: [
|
||||||
{
|
{
|
||||||
name: "user_id1",
|
name: "@user_id1:provider",
|
||||||
password_hash: "password_hash1",
|
password_hash: "password_hash1",
|
||||||
is_guest: 0,
|
is_guest: 0,
|
||||||
admin: 0,
|
admin: 0,
|
||||||
|
@ -27,7 +27,7 @@ describe("dataProvider", () => {
|
||||||
displayname: "User One",
|
displayname: "User One",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "user_id2",
|
name: "@user_id2:provider",
|
||||||
password_hash: "password_hash2",
|
password_hash: "password_hash2",
|
||||||
is_guest: 0,
|
is_guest: 0,
|
||||||
admin: 1,
|
admin: 1,
|
||||||
|
@ -47,7 +47,7 @@ describe("dataProvider", () => {
|
||||||
filter: { author_id: 12 },
|
filter: { author_id: 12 },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(users.data[0].id).toEqual("user_id1");
|
expect(users.data[0].id).toEqual("@user_id1:provider");
|
||||||
expect(users.total).toEqual(200);
|
expect(users.total).toEqual(200);
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
@ -55,7 +55,7 @@ describe("dataProvider", () => {
|
||||||
it("fetches one user", async () => {
|
it("fetches one user", async () => {
|
||||||
fetchMock.mockResponseOnce(
|
fetchMock.mockResponseOnce(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
name: "user_id1",
|
name: "@user_id1:provider",
|
||||||
password: "user_password",
|
password: "user_password",
|
||||||
displayname: "User",
|
displayname: "User",
|
||||||
threepids: [
|
threepids: [
|
||||||
|
@ -74,9 +74,9 @@ describe("dataProvider", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = await dataProvider.getOne("users", { id: "user_id1" });
|
const user = await dataProvider.getOne("users", { id: "@user_id1:provider" });
|
||||||
|
|
||||||
expect(user.data.id).toEqual("user_id1");
|
expect(user.data.id).toEqual("@user_id1:provider");
|
||||||
expect(user.data.displayname).toEqual("User");
|
expect(user.data.displayname).toEqual("User");
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
import { stringify } from "query-string";
|
import { stringify } from "query-string";
|
||||||
|
|
||||||
import { DataProvider, DeleteParams, HttpError, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
|
import {
|
||||||
|
DataProvider,
|
||||||
|
DeleteParams,
|
||||||
|
HttpError,
|
||||||
|
Identifier,
|
||||||
|
Options,
|
||||||
|
RaRecord,
|
||||||
|
UpdateParams,
|
||||||
|
fetchUtils,
|
||||||
|
withLifecycleCallbacks,
|
||||||
|
} from "react-admin";
|
||||||
|
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
import { returnMXID } from "./synapse.ts"
|
import { returnMXID } from "./synapse";
|
||||||
import { MatrixError, displayError } from "../components/error";
|
import { MatrixError, displayError } from "../components/error";
|
||||||
|
|
||||||
// Adds the access token to all requests
|
// Adds the access token to all requests
|
||||||
const jsonClient = async (url: string, options: Options = {}) => {
|
const jsonClient = async (url: string, options: Options = {}) => {
|
||||||
const token = storage.getItem("access_token");
|
const token = storage.getItem("access_token");
|
||||||
console.log("httpClient " + url);
|
console.log("httpClient " + url);
|
||||||
if (token != null) {
|
if (token !== null) {
|
||||||
options.user = {
|
options.user = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
token: `Bearer ${token}`,
|
token: `Bearer ${token}`,
|
||||||
|
@ -23,7 +33,9 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
||||||
const error = err as HttpError;
|
const error = err as HttpError;
|
||||||
const errorStatus = error.status;
|
const errorStatus = error.status;
|
||||||
const errorBody = error.body as MatrixError;
|
const errorBody = error.body as MatrixError;
|
||||||
const errMsg = !!errorBody?.errcode ? displayError(errorBody.errcode, errorStatus, errorBody.error) : displayError("M_INVALID", errorStatus, error.message);
|
const errMsg = !!errorBody?.errcode
|
||||||
|
? displayError(errorBody.errcode, errorStatus, errorBody.error)
|
||||||
|
: displayError("M_INVALID", errorStatus, error.message);
|
||||||
|
|
||||||
return Promise.reject(new HttpError(errMsg, errorStatus, errorBody));
|
return Promise.reject(new HttpError(errMsg, errorStatus, errorBody));
|
||||||
}
|
}
|
||||||
|
@ -226,8 +238,19 @@ export interface DeleteMediaResult {
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UploadMediaParams {
|
||||||
|
file: File;
|
||||||
|
filename: string;
|
||||||
|
content_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadMediaResult {
|
||||||
|
content_uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SynapseDataProvider extends DataProvider {
|
export interface SynapseDataProvider extends DataProvider {
|
||||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
|
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
|
@ -500,7 +523,7 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataProvider: SynapseDataProvider = {
|
const baseDataProvider: SynapseDataProvider = {
|
||||||
getList: async (resource, params) => {
|
getList: async (resource, params) => {
|
||||||
console.log("getList " + resource);
|
console.log("getList " + resource);
|
||||||
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
||||||
|
@ -742,6 +765,46 @@ const dataProvider: SynapseDataProvider = {
|
||||||
const { json } = await jsonClient(endpoint_url, { method: "POST" });
|
const { json } = await jsonClient(endpoint_url, { method: "POST" });
|
||||||
return json as DeleteMediaResult;
|
return json as DeleteMediaResult;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
|
||||||
|
const base_url = storage.getItem("base_url");
|
||||||
|
const uploadMediaURL = `${base_url}/_matrix/media/v3/upload`;
|
||||||
|
|
||||||
|
const { json } = await jsonClient(`${uploadMediaURL}?filename=${filename}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: file,
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": content_type,
|
||||||
|
}) as Headers,
|
||||||
|
});
|
||||||
|
return json as UploadMediaResult;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
||||||
|
{
|
||||||
|
resource: "users",
|
||||||
|
beforeUpdate: async (params: UpdateParams<any>, dataProvider: DataProvider) => {
|
||||||
|
const avatarFile = params.data.avatar_file?.rawFile;
|
||||||
|
const avatarErase = params.data.avatar_erase;
|
||||||
|
|
||||||
|
if (avatarErase) {
|
||||||
|
params.data.avatar_url = "";
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatarFile instanceof File) {
|
||||||
|
const reponse = await dataProvider.uploadMedia({
|
||||||
|
file: avatarFile,
|
||||||
|
filename: params.data.avatar_file.title,
|
||||||
|
content_type: params.data.avatar_file.rawFile.type,
|
||||||
|
});
|
||||||
|
params.data.avatar_url = reponse.content_uri;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export default dataProvider;
|
export default dataProvider;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { fetchUtils } from "react-admin";
|
import { Identifier, fetchUtils } from "react-admin";
|
||||||
|
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
|
|
||||||
|
@ -77,17 +77,17 @@ export function generateRandomMxId(): string {
|
||||||
* @param input the input string
|
* @param input the input string
|
||||||
* @returns full MXID as string
|
* @returns full MXID as string
|
||||||
*/
|
*/
|
||||||
export function returnMXID(input: string): string {
|
export function returnMXID(input: string | Identifier): string {
|
||||||
const homeserver = storage.getItem("home_server");
|
const homeserver = storage.getItem("home_server");
|
||||||
|
|
||||||
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
||||||
const mxidPattern = /^@[^@:]+:[^@:]+$/;
|
const mxidPattern = /^@[^@:]+:[^@:]+$/;
|
||||||
if (mxidPattern.test(input)) {
|
if (typeof input === 'string' && mxidPattern.test(input)) {
|
||||||
return input; // Already a valid MXID
|
return input; // Already a valid MXID
|
||||||
}
|
}
|
||||||
|
|
||||||
// If input is not a valid MXID, assume it's a localpart and construct the MXID
|
// If input is not a valid MXID, assume it's a localpart and construct the MXID
|
||||||
const localpart = input.startsWith('@') ? input.slice(1) : input;
|
const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input;
|
||||||
return `@${localpart}:${homeserver}`;
|
return `@${localpart}:${homeserver}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue