Compare commits
39 Commits
fb35fca56e
...
20251204
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a4570a044 | |||
| e51fc6b4bb | |||
| 0f68d59798 | |||
| 5ad23005be | |||
| 4e096a1d49 | |||
| ac2a361b77 | |||
| 24f8d67020 | |||
| 5bcee2ea9d | |||
| 48d9444dde | |||
| bcdfe87107 | |||
| 5088699c15 | |||
| 854b474970 | |||
| 336aea463b | |||
| fe9c692e12 | |||
| b47ec37a76 | |||
| 996534c62b | |||
| 3ba6543cb4 | |||
| f087b27b53 | |||
| dfcf764a9b | |||
| 8b299bcf8f | |||
| 1fa98cf6e3 | |||
| e80d54d0e7 | |||
| b91b61f4f0 | |||
| f0e8c799ff | |||
| b4e7cb8718 | |||
| 7a590e882b | |||
| 9a643ced94 | |||
| 5f2a6478a7 | |||
| 1db929a31b | |||
| 0b2c4071e8 | |||
| 61ecfc5af1 | |||
| 661793f58d | |||
| d253e73099 | |||
| f0d3d311e9 | |||
| 592203aa4a | |||
| aeb35029c3 | |||
| 1dc56d5ec1 | |||
| 51b1ab380c | |||
| b5abddaacb |
@@ -53,7 +53,6 @@ steps:
|
|||||||
- cd matrixgw_backend
|
- cd matrixgw_backend
|
||||||
- cargo test
|
- cargo test
|
||||||
|
|
||||||
|
|
||||||
- name: backend_build
|
- name: backend_build
|
||||||
image: rust
|
image: rust
|
||||||
when:
|
when:
|
||||||
@@ -90,7 +89,7 @@ steps:
|
|||||||
path: /tmp/release
|
path: /tmp/release
|
||||||
environment:
|
environment:
|
||||||
PLUGIN_API_KEY:
|
PLUGIN_API_KEY:
|
||||||
from_secret: API_KEY
|
from_secret: GITEA_API_KEY # needs permission write:repository
|
||||||
settings:
|
settings:
|
||||||
base_url: https://gitea.communiquons.org
|
base_url: https://gitea.communiquons.org
|
||||||
files: /tmp/release/*
|
files: /tmp/release/*
|
||||||
|
|||||||
528
matrixgw_backend/Cargo.lock
generated
528
matrixgw_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,29 +5,30 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
log = "0.4.28"
|
log = "0.4.29"
|
||||||
clap = { version = "4.5.51", features = ["derive", "env"] }
|
clap = { version = "4.5.53", features = ["derive", "env"] }
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
actix-web = "4.11.0"
|
actix-web = "4.12.1"
|
||||||
actix-session = { version = "0.11.0", features = ["redis-session"] }
|
actix-session = { version = "0.11.0", features = ["redis-session"] }
|
||||||
actix-remote-ip = "0.1.0"
|
actix-remote-ip = "0.1.0"
|
||||||
actix-cors = "0.7.1"
|
actix-cors = "0.7.1"
|
||||||
light-openid = "1.0.4"
|
light-openid = "1.0.4"
|
||||||
bytes = "1.10.1"
|
bytes = "1.11.0"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
base16ct = { version = "0.3.0", features = ["alloc"] }
|
base16ct = { version = "0.3.0", features = ["alloc"] }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] }
|
jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] }
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
uuid = { version = "1.18.1", features = ["v4", "serde"] }
|
uuid = { version = "1.19.0", features = ["v4", "serde"] }
|
||||||
ipnet = { version = "2.11.0", features = ["serde"] }
|
ipnet = { version = "2.11.0", features = ["serde"] }
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
mailchecker = "6.0.19"
|
mailchecker = "6.0.19"
|
||||||
matrix-sdk = { version = "0.14.0" }
|
matrix-sdk = { version = "0.14.0" }
|
||||||
|
matrix-sdk-ui = "0.14.0"
|
||||||
url = "2.5.7"
|
url = "2.5.7"
|
||||||
ractor = "0.15.9"
|
ractor = "0.15.9"
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::controllers::HttpResult;
|
||||||
|
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
use matrix_sdk_ui::spaces::SpaceService;
|
||||||
|
use matrix_sdk_ui::spaces::room_list::SpaceRoomListPaginationState;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Get space hierarchy
|
||||||
|
pub async fn hierarchy(client: MatrixClientExtractor) -> HttpResult {
|
||||||
|
let spaces = client.client.client.joined_space_rooms();
|
||||||
|
let space_service = SpaceService::new(client.client.client);
|
||||||
|
let mut hierarchy = HashMap::new();
|
||||||
|
for space in spaces {
|
||||||
|
let rooms = space_service.space_room_list(space.room_id().to_owned());
|
||||||
|
while !matches!(
|
||||||
|
rooms.pagination_state(),
|
||||||
|
SpaceRoomListPaginationState::Idle { end_reached: true }
|
||||||
|
) {
|
||||||
|
rooms.paginate().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
hierarchy.insert(
|
||||||
|
space.room_id().to_owned(),
|
||||||
|
rooms
|
||||||
|
.rooms()
|
||||||
|
.into_iter()
|
||||||
|
.map(|room| room.room_id)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(HttpResponse::Ok().json(hierarchy))
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ pub mod matrix_event_controller;
|
|||||||
pub mod matrix_media_controller;
|
pub mod matrix_media_controller;
|
||||||
pub mod matrix_profile_controller;
|
pub mod matrix_profile_controller;
|
||||||
pub mod matrix_room_controller;
|
pub mod matrix_room_controller;
|
||||||
|
pub mod matrix_space_controller;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use matrixgw_backend::broadcast_messages::BroadcastMessage;
|
|||||||
use matrixgw_backend::constants;
|
use matrixgw_backend::constants;
|
||||||
use matrixgw_backend::controllers::matrix::{
|
use matrixgw_backend::controllers::matrix::{
|
||||||
matrix_event_controller, matrix_media_controller, matrix_profile_controller,
|
matrix_event_controller, matrix_media_controller, matrix_profile_controller,
|
||||||
matrix_room_controller,
|
matrix_room_controller, matrix_space_controller,
|
||||||
};
|
};
|
||||||
use matrixgw_backend::controllers::{
|
use matrixgw_backend::controllers::{
|
||||||
auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller,
|
auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller,
|
||||||
@@ -138,6 +138,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
web::get().to(matrix_sync_thread_controller::status),
|
web::get().to(matrix_sync_thread_controller::status),
|
||||||
)
|
)
|
||||||
.service(web::resource("/api/ws").route(web::get().to(ws_controller::ws)))
|
.service(web::resource("/api/ws").route(web::get().to(ws_controller::ws)))
|
||||||
|
// Matrix spaces controller
|
||||||
|
.route(
|
||||||
|
"/api/matrix/space/hierarchy",
|
||||||
|
web::get().to(matrix_space_controller::hierarchy),
|
||||||
|
)
|
||||||
// Matrix room controller
|
// Matrix room controller
|
||||||
.route(
|
.route(
|
||||||
"/api/matrix/room/joined",
|
"/api/matrix/room/joined",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default defineConfig([
|
|||||||
extends: [
|
extends: [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
reactHooks.configs["recommended-latest"],
|
reactHooks.configs.flat.recommended,
|
||||||
reactRefresh.configs.vite,
|
reactRefresh.configs.vite,
|
||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
|||||||
1003
matrixgw_frontend/package-lock.json
generated
1003
matrixgw_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,14 +12,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@fontsource/roboto": "^5.2.8",
|
"@fontsource/roboto": "^5.2.9",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mui/icons-material": "^7.3.6",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mui/material": "^7.3.6",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/x-data-grid": "^8.20.0",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/x-date-pickers": "^8.19.0",
|
||||||
"@mui/x-data-grid": "^8.18.0",
|
"date-and-time": "^4.1.1",
|
||||||
"@mui/x-date-pickers": "^8.17.0",
|
|
||||||
"date-and-time": "^4.1.0",
|
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"emoji-picker-react": "^4.16.1",
|
"emoji-picker-react": "^4.16.1",
|
||||||
"is-cidr": "^6.0.1",
|
"is-cidr": "^6.0.1",
|
||||||
@@ -28,23 +26,23 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-favicon": "^2.0.7",
|
"react-favicon": "^2.0.7",
|
||||||
"react-json-view-lite": "^2.5.0",
|
"react-json-view-lite": "^2.5.0",
|
||||||
"react-router": "^7.9.5"
|
"react-router": "^7.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.39.1",
|
||||||
"@types/node": "^24.6.0",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.1.16",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.22",
|
"eslint-plugin-react-refresh": "^0.4.22",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.5.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.45.0",
|
"typescript-eslint": "^8.48.1",
|
||||||
"vite": "npm:rolldown-vite@7.1.14"
|
"vite": "npm:rolldown-vite@7.2.10"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "npm:rolldown-vite@7.1.14"
|
"vite": "npm:rolldown-vite@7.2.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export interface RoomMessageEvent extends BaseRoomEvent {
|
|||||||
"m.relates_to"?: {
|
"m.relates_to"?: {
|
||||||
rel_type?: "m.replace" | string;
|
rel_type?: "m.replace" | string;
|
||||||
event_id?: string;
|
event_id?: string;
|
||||||
|
"m.in_reply_to"?:{
|
||||||
|
event_id?:string
|
||||||
|
}
|
||||||
};
|
};
|
||||||
"m.new_content"?: {
|
"m.new_content"?: {
|
||||||
msgtype?: MessageType;
|
msgtype?: MessageType;
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ export interface MatrixRoomMessage {
|
|||||||
body: string;
|
body: string;
|
||||||
msgtype: MessageType;
|
msgtype: MessageType;
|
||||||
"m.relates_to"?: {
|
"m.relates_to"?: {
|
||||||
event_id: string;
|
event_id?: string;
|
||||||
rel_type: "m.replace" | string;
|
rel_type?: "m.replace" | string;
|
||||||
|
"m.in_reply_to"?: {
|
||||||
|
event_id?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
url?: string;
|
url?: string;
|
||||||
file?: {
|
file?: {
|
||||||
@@ -71,8 +74,8 @@ export class MatrixApiEvent {
|
|||||||
await APIClient.exec({
|
await APIClient.exec({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
uri:
|
uri:
|
||||||
`/matrix/room/${encodeURIComponent(room.id)}/events` +
|
`/matrix/room/${encodeURIComponent(room.id)}/events?limit=200` +
|
||||||
(from ? `?from=${from}` : ""),
|
(from ? `&from=${from}` : ""),
|
||||||
})
|
})
|
||||||
).data as MatrixEventsList;
|
).data as MatrixEventsList;
|
||||||
}
|
}
|
||||||
|
|||||||
40
matrixgw_frontend/src/api/matrix/MatrixApiSpace.ts
Normal file
40
matrixgw_frontend/src/api/matrix/MatrixApiSpace.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { APIClient } from "../ApiClient";
|
||||||
|
|
||||||
|
export type SpaceHierarchy = Map<string, string[]>;
|
||||||
|
|
||||||
|
export class MatrixApiSpace {
|
||||||
|
/**
|
||||||
|
* Request Matrix space hierarchy
|
||||||
|
*/
|
||||||
|
static async Hierarchy(): Promise<SpaceHierarchy> {
|
||||||
|
const hierarchy = new Map(
|
||||||
|
Object.entries(
|
||||||
|
(
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: "/matrix/space/hierarchy",
|
||||||
|
})
|
||||||
|
).data as { [s: string]: string[] }
|
||||||
|
)
|
||||||
|
) as SpaceHierarchy;
|
||||||
|
|
||||||
|
// Simplify hierarchy
|
||||||
|
while (true) {
|
||||||
|
let changed = false;
|
||||||
|
for (const [roomid, children] of hierarchy) {
|
||||||
|
for (const child of children) {
|
||||||
|
if (!hierarchy.has(child)) continue;
|
||||||
|
hierarchy.set(roomid, [
|
||||||
|
...hierarchy.get(roomid)!,
|
||||||
|
...hierarchy.get(child)!,
|
||||||
|
]);
|
||||||
|
hierarchy.delete(child);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changed) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hierarchy;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
matrixgw_frontend/src/icons/AppIcon.tsx
Normal file
18
matrixgw_frontend/src/icons/AppIcon.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Icon } from "@mui/material";
|
||||||
|
import { useActualColorMode } from "../widgets/dashboard/ThemeSwitcher";
|
||||||
|
|
||||||
|
export function AppIcon(p: { src: string; size?: string }): React.ReactElement {
|
||||||
|
const { mode } = useActualColorMode();
|
||||||
|
return (
|
||||||
|
<Icon style={{ display: "inline-flex", width: p.size, height: p.size }}>
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: mode === "dark" ? "white" : "black",
|
||||||
|
mask: `url("${p.src}")`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
matrixgw_frontend/src/icons/message-text-fast.svg
Normal file
1
matrixgw_frontend/src/icons/message-text-fast.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9 5C7.9 5 7 5.9 7 7V21L11 17H20C21.1 17 22 16.1 22 15V7C22 5.9 21.1 5 20 5H9M3 7C2.4 7 2 7.4 2 8S2.4 9 3 9H5V7H3M11 8H19V10H11V8M2 11C1.4 11 1 11.4 1 12S1.4 13 2 13H5V11H2M11 12H16V14H11V12M1 15C.4 15 0 15.4 0 16C0 16.6 .4 17 1 17H5V15H1Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 318 B |
1
matrixgw_frontend/src/icons/openid.svg
Normal file
1
matrixgw_frontend/src/icons/openid.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14,2L11,3.5V19.94C7,19.5 4,17.46 4,15C4,12.75 6.5,10.85 10,10.22V8.19C4.86,8.88 1,11.66 1,15C1,18.56 5.36,21.5 11,21.94C11.03,21.94 11.06,21.94 11.09,21.94L14,20.5V2M15,8.19V10.22C16.15,10.43 17.18,10.77 18.06,11.22L16.5,12L23,13.5L22.5,9L20.5,10C19,9.12 17.12,8.47 15,8.19Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 354 B |
@@ -19,7 +19,7 @@ import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider";
|
|||||||
import { time } from "../utils/DateUtils";
|
import { time } from "../utils/DateUtils";
|
||||||
|
|
||||||
export function APITokensRoute(): React.ReactElement {
|
export function APITokensRoute(): React.ReactElement {
|
||||||
const count = React.useRef(0);
|
const [count, setCount] = React.useState(0);
|
||||||
|
|
||||||
const [openCreateTokenDialog, setOpenCreateTokenDialog] =
|
const [openCreateTokenDialog, setOpenCreateTokenDialog] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
@@ -34,7 +34,7 @@ export function APITokensRoute(): React.ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRefreshTokensList = () => {
|
const handleRefreshTokensList = () => {
|
||||||
count.current += 1;
|
setCount((c) => c + 1);
|
||||||
setList(undefined);
|
setList(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ export function APITokensRoute(): React.ReactElement {
|
|||||||
|
|
||||||
{/* Tokens list */}
|
{/* Tokens list */}
|
||||||
<AsyncWidget
|
<AsyncWidget
|
||||||
loadKey={count.current}
|
loadKey={count}
|
||||||
ready={list !== undefined}
|
ready={list !== undefined}
|
||||||
load={load}
|
load={load}
|
||||||
errMsg="Failed to load the list of tokens!"
|
errMsg="Failed to load the list of tokens!"
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export function MatrixAuthCallback(): React.ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
load();
|
load();
|
||||||
}, [code, info, navigate, snackbar, state]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [code, state]);
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -23,13 +23,21 @@ export function WSDebugRoute(): React.ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MatrixGWRouteContainer label={"WebSocket Debug"}>
|
<MatrixGWRouteContainer label={"WebSocket Debug"}>
|
||||||
<div>
|
{/* Status bar */}
|
||||||
State:{" "}
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<span style={{ color: state == WSState.Connected ? "green" : "red" }}>
|
<span style={{ marginRight: "0.5em" }}>State: </span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginRight: "0.5em",
|
||||||
|
color: state == WSState.Connected ? "green" : "red",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{state}
|
{state}
|
||||||
</span>
|
</span>
|
||||||
<MatrixWS onStateChange={setState} onMessage={handleMessage} />
|
<MatrixWS onStateChange={setState} onMessage={handleMessage} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* WS messages list */}
|
||||||
{messages.map((msg, id) => (
|
{messages.map((msg, id) => (
|
||||||
<div style={{ margin: "10px", backgroundColor: "black" }}>
|
<div style={{ margin: "10px", backgroundColor: "black" }}>
|
||||||
<JsonView
|
<JsonView
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
||||||
import Icon from "@mdi/react";
|
|
||||||
import { mdiOpenid } from "@mdi/js";
|
|
||||||
import { ServerApi } from "../../api/ServerApi";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AuthApi } from "../../api/AuthApi";
|
import { AuthApi } from "../../api/AuthApi";
|
||||||
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
|
import { AppIcon } from "../../icons/AppIcon";
|
||||||
|
import openid from "../../icons/openid.svg";
|
||||||
|
|
||||||
export function LoginRoute(): React.ReactElement {
|
export function LoginRoute(): React.ReactElement {
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
@@ -40,7 +40,7 @@ export function LoginRoute(): React.ReactElement {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={authWithOpenID}
|
onClick={authWithOpenID}
|
||||||
startIcon={<Icon path={mdiOpenid} size={1} />}
|
startIcon={<AppIcon src={openid} />}
|
||||||
>
|
>
|
||||||
Sign in with {ServerApi.Config.oidc_provider_name}
|
Sign in with {ServerApi.Config.oidc_provider_name}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface Message {
|
|||||||
time_sent: number;
|
time_sent: number;
|
||||||
time_sent_dayjs: dayjs.Dayjs;
|
time_sent_dayjs: dayjs.Dayjs;
|
||||||
modified: boolean;
|
modified: boolean;
|
||||||
|
inReplyTo?: string;
|
||||||
reactions: Map<string, MessageReaction[]>;
|
reactions: Map<string, MessageReaction[]>;
|
||||||
content: string;
|
content: string;
|
||||||
type: MessageType;
|
type: MessageType;
|
||||||
@@ -37,7 +38,7 @@ export class RoomEventsManager {
|
|||||||
receiptsEventsMap: Map<string, Receipt[]>;
|
receiptsEventsMap: Map<string, Receipt[]>;
|
||||||
|
|
||||||
get canLoadOlder(): boolean {
|
get canLoadOlder(): boolean {
|
||||||
return !!this.endToken;
|
return !!this.endToken && this.events.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -92,13 +93,7 @@ export class RoomEventsManager {
|
|||||||
content: {
|
content: {
|
||||||
body: m.data["m.new_content"]?.body ?? m.data.body,
|
body: m.data["m.new_content"]?.body ?? m.data.body,
|
||||||
msgtype: m.data.msgtype,
|
msgtype: m.data.msgtype,
|
||||||
"m.relates_to":
|
"m.relates_to": m.data["m.relates_to"],
|
||||||
m.data["m.relates_to"] && m.data["m.relates_to"].event_id
|
|
||||||
? {
|
|
||||||
event_id: m.data["m.relates_to"].event_id!,
|
|
||||||
rel_type: m.data["m.relates_to"].rel_type ?? "",
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
url: m.data.url,
|
url: m.data.url,
|
||||||
file: m.data.file,
|
file: m.data.file,
|
||||||
},
|
},
|
||||||
@@ -174,7 +169,7 @@ export class RoomEventsManager {
|
|||||||
// Message
|
// Message
|
||||||
if (data.type === "m.room.message") {
|
if (data.type === "m.room.message") {
|
||||||
// Check if this message replaces another one
|
// Check if this message replaces another one
|
||||||
if (data.content["m.relates_to"]) {
|
if (data.content["m.relates_to"]?.rel_type === "replace") {
|
||||||
const message = this.messages.find(
|
const message = this.messages.find(
|
||||||
(m) => m.event_id === data.content["m.relates_to"]?.event_id
|
(m) => m.event_id === data.content["m.relates_to"]?.event_id
|
||||||
);
|
);
|
||||||
@@ -206,6 +201,7 @@ export class RoomEventsManager {
|
|||||||
event_id: evt.id,
|
event_id: evt.id,
|
||||||
account: evt.sender,
|
account: evt.sender,
|
||||||
modified: false,
|
modified: false,
|
||||||
|
inReplyTo: data.content["m.relates_to"]?.["m.in_reply_to"]?.event_id,
|
||||||
reactions: new Map(),
|
reactions: new Map(),
|
||||||
time_sent: evt.time,
|
time_sent: evt.time,
|
||||||
time_sent_dayjs: dayjs.unix(evt.time / 1000),
|
time_sent_dayjs: dayjs.unix(evt.time / 1000),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import React from "react";
|
||||||
|
|
||||||
const State = {
|
const State = {
|
||||||
Loading: 0,
|
Loading: 0,
|
||||||
@@ -17,9 +17,7 @@ export function AsyncWidget(p: {
|
|||||||
ready?: boolean;
|
ready?: boolean;
|
||||||
errAdditionalElement?: () => React.ReactElement;
|
errAdditionalElement?: () => React.ReactElement;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const [state, setState] = useState<number>(State.Loading);
|
const [state, setState] = React.useState<number>(State.Loading);
|
||||||
|
|
||||||
const counter = useRef<unknown>(null);
|
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -32,12 +30,10 @@ export function AsyncWidget(p: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (counter.current === p.loadKey) return;
|
|
||||||
counter.current = p.loadKey;
|
|
||||||
|
|
||||||
load();
|
load();
|
||||||
});
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [p.loadKey]);
|
||||||
|
|
||||||
if (state === State.Error)
|
if (state === State.Error)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,9 +16,16 @@ function emojiUnicode(emoji: string): string {
|
|||||||
return s.includes("f") ? s : `${s}-fe0f`;
|
return s.includes("f") ? s : `${s}-fe0f`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmojiIcon(p: { emojiKey: string }): React.ReactElement {
|
export function EmojiIcon(p: {
|
||||||
|
emojiKey: string;
|
||||||
|
size?: number;
|
||||||
|
}): React.ReactElement {
|
||||||
const unified = emojiUnicode(p.emojiKey);
|
const unified = emojiUnicode(p.emojiKey);
|
||||||
return (
|
return (
|
||||||
<Emoji unified={unified ?? ""} emojiStyle={EmojiStyle.NATIVE} size={18} />
|
<Emoji
|
||||||
|
unified={unified ?? ""}
|
||||||
|
emojiStyle={EmojiStyle.NATIVE}
|
||||||
|
size={p.size ?? 18}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { mdiMessageTextFast } from "@mdi/js";
|
|
||||||
import Icon from "@mdi/react";
|
|
||||||
import { Typography } from "@mui/material";
|
import { Typography } from "@mui/material";
|
||||||
import MuiCard from "@mui/material/Card";
|
import MuiCard from "@mui/material/Card";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import { styled } from "@mui/material/styles";
|
import { styled } from "@mui/material/styles";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
import { AppIcon } from "../../icons/AppIcon";
|
||||||
|
import mdiMessageTextFast from "../../icons/message-text-fast.svg";
|
||||||
|
|
||||||
const Card = styled(MuiCard)(({ theme }) => ({
|
const Card = styled(MuiCard)(({ theme }) => ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -57,12 +57,7 @@ export function BaseLoginPage(): React.ReactElement {
|
|||||||
variant="h4"
|
variant="h4"
|
||||||
sx={{ width: "100%", fontSize: "clamp(2rem, 10vw, 2.15rem)" }}
|
sx={{ width: "100%", fontSize: "clamp(2rem, 10vw, 2.15rem)" }}
|
||||||
>
|
>
|
||||||
<Icon
|
<AppIcon src={mdiMessageTextFast} size={"2em"} /> MatrixGW
|
||||||
path={mdiMessageTextFast}
|
|
||||||
size={"1em"}
|
|
||||||
style={{ display: "inline-table" }}
|
|
||||||
/>{" "}
|
|
||||||
MatrixGW
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { mdiMessageTextFast } from "@mdi/js";
|
|
||||||
import Icon from "@mdi/react";
|
|
||||||
import LogoutIcon from "@mui/icons-material/Logout";
|
import LogoutIcon from "@mui/icons-material/Logout";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
|
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
|
||||||
@@ -13,6 +11,8 @@ import Toolbar from "@mui/material/Toolbar";
|
|||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { AppIcon } from "../../icons/AppIcon";
|
||||||
|
import mdiMessageTextFast from "../../icons/message-text-fast.svg";
|
||||||
import { RouterLink } from "../RouterLink";
|
import { RouterLink } from "../RouterLink";
|
||||||
import { useUserInfo } from "./BaseAuthenticatedPage";
|
import { useUserInfo } from "./BaseAuthenticatedPage";
|
||||||
import ThemeSwitcher from "./ThemeSwitcher";
|
import ThemeSwitcher from "./ThemeSwitcher";
|
||||||
@@ -101,7 +101,7 @@ export default function DashboardHeader({
|
|||||||
<RouterLink to="/">
|
<RouterLink to="/">
|
||||||
<Stack direction="row" alignItems="center">
|
<Stack direction="row" alignItems="center">
|
||||||
<LogoContainer>
|
<LogoContainer>
|
||||||
<Icon path={mdiMessageTextFast} size="2em" />
|
<AppIcon src={mdiMessageTextFast} size="2em" />
|
||||||
</LogoContainer>
|
</LogoContainer>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { mdiBug, mdiForum, mdiKeyVariant, mdiLinkLock } from "@mdi/js";
|
import BugReportIcon from "@mui/icons-material/BugReport";
|
||||||
import Icon from "@mdi/react";
|
import ForumIcon from "@mui/icons-material/Forum";
|
||||||
|
import KeyIcon from "@mui/icons-material/Key";
|
||||||
|
import LinkIcon from "@mui/icons-material/Link";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Drawer from "@mui/material/Drawer";
|
import Drawer from "@mui/material/Drawer";
|
||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
@@ -98,27 +100,27 @@ export default function DashboardSidebar({
|
|||||||
<DashboardSidebarPageItem
|
<DashboardSidebarPageItem
|
||||||
disabled={!user.info.matrix_account_connected}
|
disabled={!user.info.matrix_account_connected}
|
||||||
title="Messages"
|
title="Messages"
|
||||||
icon={<Icon path={mdiForum} size={"1.5em"} />}
|
icon={<ForumIcon style={{ height: "1em", width: "1em" }} />}
|
||||||
href="/"
|
href="/"
|
||||||
mini={viewport === "desktop"}
|
mini={viewport === "desktop"}
|
||||||
/>
|
/>
|
||||||
<DashboardSidebarDividerItem />
|
<DashboardSidebarDividerItem />
|
||||||
<DashboardSidebarPageItem
|
<DashboardSidebarPageItem
|
||||||
title="Matrix link"
|
title="Matrix link"
|
||||||
icon={<Icon path={mdiLinkLock} size={"1.5em"} />}
|
icon={<LinkIcon style={{ height: "1em", width: "1em" }} />}
|
||||||
href="/matrix_link"
|
href="/matrix_link"
|
||||||
mini={viewport === "desktop"}
|
mini={viewport === "desktop"}
|
||||||
/>
|
/>
|
||||||
<DashboardSidebarPageItem
|
<DashboardSidebarPageItem
|
||||||
title="API tokens"
|
title="API tokens"
|
||||||
icon={<Icon path={mdiKeyVariant} size={"1.5em"} />}
|
icon={<KeyIcon style={{ height: "1em", width: "1em" }} />}
|
||||||
href="/tokens"
|
href="/tokens"
|
||||||
mini={viewport === "desktop"}
|
mini={viewport === "desktop"}
|
||||||
/>
|
/>
|
||||||
<DashboardSidebarPageItem
|
<DashboardSidebarPageItem
|
||||||
disabled={!user.info.matrix_account_connected}
|
disabled={!user.info.matrix_account_connected}
|
||||||
title="WS Debug"
|
title="WS Debug"
|
||||||
icon={<Icon path={mdiBug} size={"1.5em"} />}
|
icon={<BugReportIcon style={{ height: "1em", width: "1em" }} />}
|
||||||
href="/wsdebug"
|
href="/wsdebug"
|
||||||
mini={viewport === "desktop"}
|
mini={viewport === "desktop"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import DarkModeIcon from "@mui/icons-material/DarkMode";
|
|||||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||||
import type {} from "@mui/material/themeCssVarsAugmentation";
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
|
|
||||||
export default function ThemeSwitcher() {
|
export function useActualColorMode(): {
|
||||||
const theme = useTheme();
|
mode: "light" | "dark";
|
||||||
|
setMode: (mode: "light" | "dark") => void;
|
||||||
|
} {
|
||||||
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
||||||
const preferredMode = prefersDarkMode ? "dark" : "light";
|
const preferredMode = prefersDarkMode ? "dark" : "light";
|
||||||
|
|
||||||
@@ -17,21 +18,27 @@ export default function ThemeSwitcher() {
|
|||||||
|
|
||||||
const paletteMode = !mode || mode === "system" ? preferredMode : mode;
|
const paletteMode = !mode || mode === "system" ? preferredMode : mode;
|
||||||
|
|
||||||
|
return { mode: paletteMode, setMode };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThemeSwitcher() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { mode, setMode } = useActualColorMode();
|
||||||
|
|
||||||
const toggleMode = React.useCallback(() => {
|
const toggleMode = React.useCallback(() => {
|
||||||
setMode(paletteMode === "dark" ? "light" : "dark");
|
setMode(mode === "dark" ? "light" : "dark");
|
||||||
}, [setMode, paletteMode]);
|
}, [mode, setMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={`${paletteMode === "dark" ? "Light" : "Dark"} mode`}
|
title={`${mode === "dark" ? "Light" : "Dark"} mode`}
|
||||||
enterDelay={1000}
|
enterDelay={1000}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
aria-label={`Switch to ${
|
aria-label={`Switch to ${mode === "dark" ? "light" : "dark"} mode`}
|
||||||
paletteMode === "dark" ? "light" : "dark"
|
|
||||||
} mode`}
|
|
||||||
onClick={toggleMode}
|
onClick={toggleMode}
|
||||||
>
|
>
|
||||||
<LightModeIcon
|
<LightModeIcon
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
type UsersMap,
|
type UsersMap,
|
||||||
} from "../../api/matrix/MatrixApiProfile";
|
} from "../../api/matrix/MatrixApiProfile";
|
||||||
import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom";
|
import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom";
|
||||||
|
import {
|
||||||
|
MatrixApiSpace,
|
||||||
|
type SpaceHierarchy,
|
||||||
|
} from "../../api/matrix/MatrixApiSpace";
|
||||||
import { MatrixSyncApi } from "../../api/MatrixSyncApi";
|
import { MatrixSyncApi } from "../../api/MatrixSyncApi";
|
||||||
import type { WsMessage } from "../../api/WsApi";
|
import type { WsMessage } from "../../api/WsApi";
|
||||||
import { RoomEventsManager } from "../../utils/RoomEventsManager";
|
import { RoomEventsManager } from "../../utils/RoomEventsManager";
|
||||||
@@ -19,13 +23,19 @@ import { SpaceSelector } from "./SpaceSelector";
|
|||||||
|
|
||||||
export function MainMessageWidget(): React.ReactElement {
|
export function MainMessageWidget(): React.ReactElement {
|
||||||
const [rooms, setRooms] = React.useState<Room[] | undefined>();
|
const [rooms, setRooms] = React.useState<Room[] | undefined>();
|
||||||
|
const [hierarchy, setHierarchy] = React.useState<
|
||||||
|
SpaceHierarchy | undefined
|
||||||
|
>();
|
||||||
const [users, setUsers] = React.useState<UsersMap | undefined>();
|
const [users, setUsers] = React.useState<UsersMap | undefined>();
|
||||||
|
|
||||||
const loadRoomsList = async () => {
|
const loadRoomsList = async () => {
|
||||||
await MatrixSyncApi.Start();
|
await MatrixSyncApi.Start();
|
||||||
|
|
||||||
const rooms = await MatrixApiRoom.ListJoined();
|
const rooms = await MatrixApiRoom.ListJoined();
|
||||||
|
const hierarchy = await MatrixApiSpace.Hierarchy();
|
||||||
|
|
||||||
setRooms(rooms);
|
setRooms(rooms);
|
||||||
|
setHierarchy(hierarchy);
|
||||||
|
|
||||||
// Get the list of users in rooms
|
// Get the list of users in rooms
|
||||||
const users = rooms.reduce((prev, r) => {
|
const users = rooms.reduce((prev, r) => {
|
||||||
@@ -40,11 +50,12 @@ export function MainMessageWidget(): React.ReactElement {
|
|||||||
<AsyncWidget
|
<AsyncWidget
|
||||||
loadKey={1}
|
loadKey={1}
|
||||||
load={loadRoomsList}
|
load={loadRoomsList}
|
||||||
ready={!!rooms && !!users}
|
ready={!!rooms && !!users && !!hierarchy}
|
||||||
errMsg="Failed to initialize messaging component!"
|
errMsg="Failed to initialize messaging component!"
|
||||||
build={() => (
|
build={() => (
|
||||||
<MainMessageWidgetInner
|
<MainMessageWidgetInner
|
||||||
rooms={rooms!}
|
rooms={rooms!}
|
||||||
|
hierarchy={hierarchy!}
|
||||||
users={users!}
|
users={users!}
|
||||||
onRoomsListUpdate={(cb) => setRooms((r) => cb(r!))}
|
onRoomsListUpdate={(cb) => setRooms((r) => cb(r!))}
|
||||||
/>
|
/>
|
||||||
@@ -55,6 +66,7 @@ export function MainMessageWidget(): React.ReactElement {
|
|||||||
|
|
||||||
function MainMessageWidgetInner(p: {
|
function MainMessageWidgetInner(p: {
|
||||||
rooms: Room[];
|
rooms: Room[];
|
||||||
|
hierarchy: SpaceHierarchy;
|
||||||
users: UsersMap;
|
users: UsersMap;
|
||||||
onRoomsListUpdate: (cb: (a: Room[]) => Room[]) => void;
|
onRoomsListUpdate: (cb: (a: Room[]) => Room[]) => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
@@ -65,11 +77,13 @@ function MainMessageWidgetInner(p: {
|
|||||||
|
|
||||||
const spaceRooms = React.useMemo(() => {
|
const spaceRooms = React.useMemo(() => {
|
||||||
return p.rooms
|
return p.rooms
|
||||||
.filter((r) => !r.is_space && (!space || r.parents.includes(space)))
|
.filter(
|
||||||
|
(r) => !r.is_space && (!space || p.hierarchy.get(space)?.includes(r.id))
|
||||||
|
)
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) => (b.latest_event?.time ?? 0) - (a.latest_event?.time ?? 0)
|
(a, b) => (b.latest_event?.time ?? 0) - (a.latest_event?.time ?? 0)
|
||||||
);
|
);
|
||||||
}, [space, p.rooms]);
|
}, [space, p.rooms, p.hierarchy]);
|
||||||
|
|
||||||
const unreadRooms = React.useMemo(
|
const unreadRooms = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -111,7 +125,7 @@ function MainMessageWidgetInner(p: {
|
|||||||
p.onRoomsListUpdate((r) => {
|
p.onRoomsListUpdate((r) => {
|
||||||
const n = [...r];
|
const n = [...r];
|
||||||
const idx = r.findIndex((el) => el.id === m.room_id);
|
const idx = r.findIndex((el) => el.id === m.room_id);
|
||||||
if (idx && n[idx].notifications === "AllMessages")
|
if (idx && n[idx]?.notifications === "AllMessages")
|
||||||
n[idx] = {
|
n[idx] = {
|
||||||
...n[idx],
|
...n[idx],
|
||||||
number_unread_messages: n[idx].number_unread_messages + 1,
|
number_unread_messages: n[idx].number_unread_messages + 1,
|
||||||
|
|||||||
@@ -55,16 +55,8 @@ export function RoomMessagesList(p: {
|
|||||||
messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
|
||||||
}, [lastEventId, messagesEndRef]);
|
}, [lastEventId, messagesEndRef]);
|
||||||
|
|
||||||
// Watch scroll to detect when user reach the top to load older messages
|
const loadOlderMessages = async () => {
|
||||||
const handleScroll = async () => {
|
if (loadingOlder || !p.manager.canLoadOlder) return;
|
||||||
if (!listContainerRef.current || loadingOlder || !p.manager.canLoadOlder)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const { scrollTop } = listContainerRef.current;
|
|
||||||
|
|
||||||
if (scrollTop !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingOlder(true);
|
setLoadingOlder(true);
|
||||||
|
|
||||||
@@ -82,6 +74,19 @@ export function RoomMessagesList(p: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Watch scroll to detect when user reach the top to load older messages
|
||||||
|
const handleScroll = async () => {
|
||||||
|
if (!listContainerRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop } = listContainerRef.current;
|
||||||
|
|
||||||
|
if (scrollTop !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOlderMessages();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
@@ -94,22 +99,8 @@ export function RoomMessagesList(p: {
|
|||||||
paddingLeft: "20px",
|
paddingLeft: "20px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Empty conversation notice */}
|
|
||||||
{p.manager.messages.length === 0 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
No message in this conversation yet!
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/** Begining of conversation */}
|
{/** Begining of conversation */}
|
||||||
{!p.manager.canLoadOlder && (
|
{!p.manager.canLoadOlder && p.manager.messages.length > 0 && (
|
||||||
<Typography
|
<Typography
|
||||||
component={"div"}
|
component={"div"}
|
||||||
variant="caption"
|
variant="caption"
|
||||||
@@ -119,6 +110,22 @@ export function RoomMessagesList(p: {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/** Load older messages button */}
|
||||||
|
{p.manager.canLoadOlder && !loadingOlder && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
padding: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={loadOlderMessages} variant="outlined">
|
||||||
|
Load older messages
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/** Loading older messages spinner */}
|
{/** Loading older messages spinner */}
|
||||||
{loadingOlder && (
|
{loadingOlder && (
|
||||||
<div
|
<div
|
||||||
@@ -133,6 +140,21 @@ export function RoomMessagesList(p: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Empty conversation notice */}
|
||||||
|
{p.manager.messages.length === 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No message in this conversation yet!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/** Messages themselves */}
|
||||||
{p.manager.messages.map((m, idx) => (
|
{p.manager.messages.map((m, idx) => (
|
||||||
<RoomMessage
|
<RoomMessage
|
||||||
key={m.event_id}
|
key={m.event_id}
|
||||||
@@ -149,6 +171,11 @@ export function RoomMessagesList(p: {
|
|||||||
p.manager.messages[idx - 1].time_sent_dayjs.startOf("day").unix()
|
p.manager.messages[idx - 1].time_sent_dayjs.startOf("day").unix()
|
||||||
}
|
}
|
||||||
receipts={p.manager.receiptsEventsMap.get(m.event_id)}
|
receipts={p.manager.receiptsEventsMap.get(m.event_id)}
|
||||||
|
repliedMessage={
|
||||||
|
(m.inReplyTo &&
|
||||||
|
p.manager.messages.find((s) => s.event_id === m.inReplyTo)) ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -164,6 +191,7 @@ function RoomMessage(p: {
|
|||||||
previousFromSamePerson: boolean;
|
previousFromSamePerson: boolean;
|
||||||
firstMessageOfDay: boolean;
|
firstMessageOfDay: boolean;
|
||||||
receipts?: Receipt[];
|
receipts?: Receipt[];
|
||||||
|
repliedMessage?: Message;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const user = useUserInfo();
|
const user = useUserInfo();
|
||||||
@@ -180,6 +208,8 @@ function RoomMessage(p: {
|
|||||||
const closeImageFullScreen = () => setShowImageFullScreen(false);
|
const closeImageFullScreen = () => setShowImageFullScreen(false);
|
||||||
|
|
||||||
const sender = p.users.get(p.message.account);
|
const sender = p.users.get(p.message.account);
|
||||||
|
const repliedMsgSender =
|
||||||
|
p.repliedMessage && p.users.get(p.repliedMessage.account);
|
||||||
|
|
||||||
const handleDeleteMessage = async () => {
|
const handleDeleteMessage = async () => {
|
||||||
if (!(await confirm(`Do you really want to delete this message?`))) return;
|
if (!(await confirm(`Do you really want to delete this message?`))) return;
|
||||||
@@ -292,12 +322,37 @@ function RoomMessage(p: {
|
|||||||
"&:hover *": { visibility: "visible" },
|
"&:hover *": { visibility: "visible" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="caption">
|
<Typography
|
||||||
{p.message.time_sent_dayjs.format("HH:mm")}
|
variant="caption"
|
||||||
|
style={{
|
||||||
|
paddingLeft: "2px",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.message.time_sent_dayjs.format("HH:mm")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/** Message itself */}
|
{/** Message itself */}
|
||||||
<div style={{ marginLeft: "15px", whiteSpace: "pre-wrap", flex: 1 }}>
|
<div style={{ marginLeft: "15px", whiteSpace: "pre-wrap", flex: 1 }}>
|
||||||
|
{/** In case of reply */}
|
||||||
|
{p.repliedMessage && repliedMsgSender && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
borderLeft: "1px red solid",
|
||||||
|
paddingLeft: "10px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccountIcon user={repliedMsgSender} size={16} />
|
||||||
|
<div style={{ marginLeft: "10px" }}>
|
||||||
|
{p.repliedMessage?.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
{p.message.type === "m.image" && (
|
{p.message.type === "m.image" && (
|
||||||
<img
|
<img
|
||||||
@@ -465,11 +520,9 @@ function RoomMessage(p: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ margin: "0px 3px" }}>
|
<div style={{ margin: "0px 3px" }}>
|
||||||
<EmojiIcon emojiKey={r} />
|
<EmojiIcon emojiKey={r} size={16} />
|
||||||
</div>
|
|
||||||
<div style={{ height: "2em", marginLeft: "2px" }}>
|
|
||||||
{reactions.length}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ marginLeft: "2px" }}>{reactions.length}</div>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import {
|
import {
|
||||||
Chip,
|
Chip,
|
||||||
List,
|
List,
|
||||||
@@ -5,6 +6,7 @@ import {
|
|||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||||
@@ -22,11 +24,19 @@ export function RoomSelector(p: {
|
|||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const user = useUserInfo();
|
const user = useUserInfo();
|
||||||
|
|
||||||
|
const [filter, setFilter] = React.useState("");
|
||||||
const [unread, setUnread] = React.useState(false);
|
const [unread, setUnread] = React.useState(false);
|
||||||
|
|
||||||
const shownRooms = React.useMemo(
|
const shownRooms = React.useMemo(
|
||||||
() => p.rooms.filter((r) => !unread || r.number_unread_messages > 0),
|
() =>
|
||||||
[p.rooms, unread]
|
p.rooms
|
||||||
|
.filter((r) => !unread || r.number_unread_messages > 0)
|
||||||
|
.filter(
|
||||||
|
(r) =>
|
||||||
|
filter === "" ||
|
||||||
|
r.name?.toLocaleLowerCase()?.includes(filter.toLocaleLowerCase())
|
||||||
|
),
|
||||||
|
[p.rooms, unread, filter]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (p.rooms.length === 0)
|
if (p.rooms.length === 0)
|
||||||
@@ -45,6 +55,19 @@ export function RoomSelector(p: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
{/** Filter bar */}
|
||||||
|
<TextField
|
||||||
|
placeholder="Filter rooms"
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: <SearchIcon style={{ marginRight: "10px" }} />,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
style={{ margin: "5px" }}
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/** Chip bar */}
|
{/** Chip bar */}
|
||||||
<div style={{ padding: "5px 10px", marginTop: "5px" }}>
|
<div style={{ padding: "5px 10px", marginTop: "5px" }}>
|
||||||
<span onClick={() => setUnread(!unread)} style={{ cursor: "pointer" }}>
|
<span onClick={() => setUnread(!unread)} style={{ cursor: "pointer" }}>
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ import { Button } from "@mui/material";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||||
import type { Room } from "../../api/matrix/MatrixApiRoom";
|
import type { Room } from "../../api/matrix/MatrixApiRoom";
|
||||||
|
import type { SpaceHierarchy } from "../../api/matrix/MatrixApiSpace";
|
||||||
import { RoomIcon } from "./RoomIcon";
|
import { RoomIcon } from "./RoomIcon";
|
||||||
|
|
||||||
export function SpaceSelector(p: {
|
export function SpaceSelector(p: {
|
||||||
rooms: Room[];
|
rooms: Room[];
|
||||||
|
hierarchy: SpaceHierarchy;
|
||||||
users: UsersMap;
|
users: UsersMap;
|
||||||
selectedSpace?: string;
|
selectedSpace?: string;
|
||||||
onChange: (space?: string) => void;
|
onChange: (space?: string) => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const spaces = React.useMemo(
|
const spaces = React.useMemo(
|
||||||
() => p.rooms.filter((r) => r.is_space),
|
() => p.rooms.filter((r) => r.is_space && p.hierarchy.has(r.id)),
|
||||||
[p.rooms]
|
[p.rooms, p.hierarchy]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Do not display space bar if your is not member of any space
|
// Do not display space bar if your is not member of any space
|
||||||
|
|||||||
Reference in New Issue
Block a user