Compare commits
107 Commits
fb35fca56e
...
renovate/n
| Author | SHA1 | Date | |
|---|---|---|---|
| acaf95b890 | |||
| 8f4480e555 | |||
| 06e1f60314 | |||
| a7432a4014 | |||
| be5e7eb328 | |||
| c1e703c4b4 | |||
| 071aad8147 | |||
| f4b3c0aa16 | |||
| daca7410d7 | |||
| 23074ac354 | |||
| 36bd8d0672 | |||
| 6162555702 | |||
| 951d0db0b7 | |||
| ef90aba489 | |||
| 6b39fd11bd | |||
| bf6561fa87 | |||
| e5feecc703 | |||
| 153ad14a51 | |||
| e5494e51a3 | |||
| f04ab4591b | |||
| ca2cdb2f79 | |||
| 795a12c8d0 | |||
| 79e78006fa | |||
| 18c0fbef3c | |||
| 0ac6fc4ac3 | |||
| 2aaced17d8 | |||
| 9da2a9e9b3 | |||
| 9595ff2e71 | |||
| 5bd62d7683 | |||
| 7e747b50f3 | |||
| a9a5d60edd | |||
| 89dbc252e8 | |||
| 3a6b2c6cf2 | |||
| 98c813b220 | |||
| ceb7859169 | |||
| 96e597ca59 | |||
| d9630fbc4c | |||
| 4d7db2de2a | |||
| 4c0be88570 | |||
| 2f933a247f | |||
| f6a7132d43 | |||
| 1089b5a6a6 | |||
| 28a1b5f4f0 | |||
| acf91c3f0e | |||
| fadb9e6d46 | |||
| 17bad4fcfd | |||
| b3dfc35103 | |||
| 602f663217 | |||
| 5ebfbf6aec | |||
| aad0a74ad5 | |||
| 382e24e17b | |||
| 1876c7b43d | |||
| 73af601a16 | |||
| 6247463c70 | |||
| 9425ed9a12 | |||
| 430ad85c37 | |||
| 29e50bd70c | |||
| 0e83e804d8 | |||
| bd674bfb67 | |||
| fec81ac92e | |||
| d3e25eed9e | |||
| ba5f5f2557 | |||
| 7e548ad5d1 | |||
| 7b63bb0d05 | |||
| 788018451a | |||
| 7b3a2d6a3f | |||
| 0d462f848d | |||
| 9c6c338919 | |||
| 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 |
183
.drone.yml
183
.drone.yml
@@ -4,102 +4,101 @@ type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
# Frontend
|
||||
- name: web_build
|
||||
image: node:23
|
||||
volumes:
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
commands:
|
||||
- node -v
|
||||
- npm -v
|
||||
- cd matrixgw_frontend
|
||||
- npm install
|
||||
- npm run lint
|
||||
- npm run build
|
||||
- mv dist /tmp/web_build
|
||||
# Frontend
|
||||
- name: web_build
|
||||
image: node:25
|
||||
volumes:
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
commands:
|
||||
- node -v
|
||||
- npm -v
|
||||
- cd matrixgw_frontend
|
||||
- npm install
|
||||
- npm run lint
|
||||
- npm run build
|
||||
- mv dist /tmp/web_build
|
||||
|
||||
# Backend
|
||||
- name: backend_fetch_deps
|
||||
image: rust
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
commands:
|
||||
- cd matrixgw_backend
|
||||
- cargo fetch
|
||||
# Backend
|
||||
- name: backend_fetch_deps
|
||||
image: rust
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
commands:
|
||||
- cd matrixgw_backend
|
||||
- cargo fetch
|
||||
|
||||
- name: backend_code_quality
|
||||
image: rust
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
depends_on:
|
||||
- backend_fetch_deps
|
||||
commands:
|
||||
- cd matrixgw_backend
|
||||
- rustup component add clippy
|
||||
- cargo clippy -- -D warnings
|
||||
- cargo clippy --example api_curl -- -D warnings
|
||||
- name: backend_code_quality
|
||||
image: rust
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
depends_on:
|
||||
- backend_fetch_deps
|
||||
commands:
|
||||
- cd matrixgw_backend
|
||||
- rustup component add clippy
|
||||
- cargo clippy -- -D warnings
|
||||
- cargo clippy --example api_curl -- -D warnings
|
||||
|
||||
- name: backend_test
|
||||
image: rust
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
depends_on:
|
||||
- backend_code_quality
|
||||
commands:
|
||||
- cd matrixgw_backend
|
||||
- cargo test
|
||||
- name: backend_test
|
||||
image: rust
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
depends_on:
|
||||
- backend_code_quality
|
||||
commands:
|
||||
- cd matrixgw_backend
|
||||
- cargo test
|
||||
|
||||
- name: backend_build
|
||||
image: rust
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
- name: release
|
||||
path: /tmp/release
|
||||
depends_on:
|
||||
- backend_test
|
||||
- web_build
|
||||
commands:
|
||||
- cd matrixgw_backend
|
||||
- mv /tmp/web_build/dist static
|
||||
- cargo build --release
|
||||
- cargo build --release --example api_curl
|
||||
- ls -lah target/release/matrixgw_backend target/release/examples/api_curl
|
||||
- cp target/release/matrixgw_backend target/release/examples/api_curl /tmp/release
|
||||
|
||||
- name: backend_build
|
||||
image: rust
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
- name: release
|
||||
path: /tmp/release
|
||||
depends_on:
|
||||
- backend_test
|
||||
- web_build
|
||||
commands:
|
||||
- cd matrixgw_backend
|
||||
- mv /tmp/web_build/dist static
|
||||
- cargo build --release
|
||||
- cargo build --release --example api_curl
|
||||
- ls -lah target/release/matrixgw_backend target/release/examples/api_curl
|
||||
- cp target/release/matrixgw_backend target/release/examples/api_curl /tmp/release
|
||||
|
||||
# Release
|
||||
- name: gitea_release
|
||||
image: plugins/gitea-release
|
||||
depends_on:
|
||||
- backend_build
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
volumes:
|
||||
- name: release
|
||||
path: /tmp/release
|
||||
environment:
|
||||
PLUGIN_API_KEY:
|
||||
from_secret: API_KEY
|
||||
settings:
|
||||
base_url: https://gitea.communiquons.org
|
||||
files: /tmp/release/*
|
||||
checksum: sha512
|
||||
# Release
|
||||
- name: gitea_release
|
||||
image: plugins/gitea-release
|
||||
depends_on:
|
||||
- backend_build
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
volumes:
|
||||
- name: release
|
||||
path: /tmp/release
|
||||
environment:
|
||||
PLUGIN_API_KEY:
|
||||
from_secret: GITEA_API_KEY # needs permission write:repository
|
||||
settings:
|
||||
base_url: https://gitea.communiquons.org
|
||||
files: /tmp/release/*
|
||||
checksum: sha512
|
||||
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
temp: {}
|
||||
- name: web_app
|
||||
temp: {}
|
||||
- name: release
|
||||
temp: {}
|
||||
- name: rust_registry
|
||||
temp: {}
|
||||
- name: web_app
|
||||
temp: {}
|
||||
- name: release
|
||||
temp: {}
|
||||
|
||||
1493
matrixgw_backend/Cargo.lock
generated
1493
matrixgw_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,32 +5,33 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.28"
|
||||
clap = { version = "4.5.51", features = ["derive", "env"] }
|
||||
log = "0.4.29"
|
||||
clap = { version = "4.5.54", features = ["derive", "env"] }
|
||||
lazy_static = "1.5.0"
|
||||
anyhow = "1.0.100"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
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-remote-ip = "0.1.0"
|
||||
actix-cors = "0.7.1"
|
||||
light-openid = "1.0.4"
|
||||
bytes = "1.10.1"
|
||||
bytes = "1.11.0"
|
||||
sha2 = "0.10.9"
|
||||
base16ct = { version = "0.3.0", features = ["alloc"] }
|
||||
futures-util = "0.3.31"
|
||||
jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] }
|
||||
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"] }
|
||||
rand = "0.9.2"
|
||||
hex = "0.4.3"
|
||||
mailchecker = "6.0.19"
|
||||
matrix-sdk = { version = "0.14.0" }
|
||||
url = "2.5.7"
|
||||
ractor = "0.15.9"
|
||||
serde_json = "1.0.145"
|
||||
matrix-sdk = { version = "0.16.0", features = ["e2e-encryption"] }
|
||||
matrix-sdk-ui = "0.16.0"
|
||||
url = "2.5.8"
|
||||
ractor = "0.15.10"
|
||||
serde_json = "1.0.149"
|
||||
lazy-regex = "3.4.2"
|
||||
actix-ws = "0.3.0"
|
||||
infer = "0.19.0"
|
||||
|
||||
@@ -4,11 +4,9 @@ use crate::utils::crypt_utils::sha512;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::http::header;
|
||||
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
|
||||
use matrix_sdk::crypto::{AttachmentDecryptor, MediaEncryptionInfo};
|
||||
use matrix_sdk::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
|
||||
use matrix_sdk::ruma::events::room::MediaSource;
|
||||
use matrix_sdk::ruma::{OwnedMxcUri, UInt};
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct MediaMXCInPath {
|
||||
@@ -54,21 +52,6 @@ pub async fn serve_media(req: HttpRequest, source: MediaSource, thumbnail: bool)
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Decrypt file if needed
|
||||
let media = if let MediaSource::Encrypted(file) = source {
|
||||
let mut cursor = Cursor::new(media);
|
||||
let mut decryptor =
|
||||
AttachmentDecryptor::new(&mut cursor, MediaEncryptionInfo::from(*file))?;
|
||||
|
||||
let mut decrypted_data = Vec::new();
|
||||
|
||||
decryptor.read_to_end(&mut decrypted_data)?;
|
||||
|
||||
decrypted_data
|
||||
} else {
|
||||
media
|
||||
};
|
||||
|
||||
let digest = sha512(&media);
|
||||
|
||||
let mime_type = infer::get(&media).map(|x| x.mime_type());
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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())
|
||||
.await;
|
||||
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_profile_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::controllers::matrix::{
|
||||
matrix_event_controller, matrix_media_controller, matrix_profile_controller,
|
||||
matrix_room_controller,
|
||||
matrix_room_controller, matrix_space_controller,
|
||||
};
|
||||
use matrixgw_backend::controllers::{
|
||||
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),
|
||||
)
|
||||
.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
|
||||
.route(
|
||||
"/api/matrix/room/joined",
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig([
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs["recommended-latest"],
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
|
||||
1020
matrixgw_frontend/package-lock.json
generated
1020
matrixgw_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,39 +12,37 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/roboto": "^5.2.8",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/x-data-grid": "^8.18.0",
|
||||
"@mui/x-date-pickers": "^8.17.0",
|
||||
"date-and-time": "^4.1.0",
|
||||
"@fontsource/roboto": "^5.2.9",
|
||||
"@mui/icons-material": "^7.3.7",
|
||||
"@mui/material": "^7.3.7",
|
||||
"@mui/x-data-grid": "^8.24.0",
|
||||
"@mui/x-date-pickers": "^8.23.0",
|
||||
"date-and-time": "^4.1.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"emoji-picker-react": "^4.16.1",
|
||||
"is-cidr": "^6.0.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-favicon": "^2.0.7",
|
||||
"react-json-view-lite": "^2.5.0",
|
||||
"react-router": "^7.9.5"
|
||||
"react-router": "^7.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"globals": "^16.4.0",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/node": "^25.0.8",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "npm:rolldown-vite@7.1.14"
|
||||
"typescript-eslint": "^8.51.0",
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@7.1.14"
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ export interface RoomMessageEvent extends BaseRoomEvent {
|
||||
"m.relates_to"?: {
|
||||
rel_type?: "m.replace" | string;
|
||||
event_id?: string;
|
||||
"m.in_reply_to"?:{
|
||||
event_id?:string
|
||||
}
|
||||
};
|
||||
"m.new_content"?: {
|
||||
msgtype?: MessageType;
|
||||
|
||||
@@ -15,8 +15,11 @@ export interface MatrixRoomMessage {
|
||||
body: string;
|
||||
msgtype: MessageType;
|
||||
"m.relates_to"?: {
|
||||
event_id: string;
|
||||
rel_type: "m.replace" | string;
|
||||
event_id?: string;
|
||||
rel_type?: "m.replace" | string;
|
||||
"m.in_reply_to"?: {
|
||||
event_id?: string;
|
||||
};
|
||||
};
|
||||
url?: string;
|
||||
file?: {
|
||||
@@ -71,8 +74,8 @@ export class MatrixApiEvent {
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri:
|
||||
`/matrix/room/${encodeURIComponent(room.id)}/events` +
|
||||
(from ? `?from=${from}` : ""),
|
||||
`/matrix/room/${encodeURIComponent(room.id)}/events?limit=200` +
|
||||
(from ? `&from=${from}` : ""),
|
||||
})
|
||||
).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";
|
||||
|
||||
export function APITokensRoute(): React.ReactElement {
|
||||
const count = React.useRef(0);
|
||||
const [count, setCount] = React.useState(0);
|
||||
|
||||
const [openCreateTokenDialog, setOpenCreateTokenDialog] =
|
||||
React.useState(false);
|
||||
@@ -34,7 +34,7 @@ export function APITokensRoute(): React.ReactElement {
|
||||
};
|
||||
|
||||
const handleRefreshTokensList = () => {
|
||||
count.current += 1;
|
||||
setCount((c) => c + 1);
|
||||
setList(undefined);
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export function APITokensRoute(): React.ReactElement {
|
||||
|
||||
{/* Tokens list */}
|
||||
<AsyncWidget
|
||||
loadKey={count.current}
|
||||
loadKey={count}
|
||||
ready={list !== undefined}
|
||||
load={load}
|
||||
errMsg="Failed to load the list of tokens!"
|
||||
|
||||
@@ -41,7 +41,8 @@ export function MatrixAuthCallback(): React.ReactElement {
|
||||
};
|
||||
|
||||
load();
|
||||
}, [code, info, navigate, snackbar, state]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [code, state]);
|
||||
|
||||
if (error)
|
||||
return (
|
||||
|
||||
@@ -23,13 +23,21 @@ export function WSDebugRoute(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<MatrixGWRouteContainer label={"WebSocket Debug"}>
|
||||
<div>
|
||||
State:{" "}
|
||||
<span style={{ color: state == WSState.Connected ? "green" : "red" }}>
|
||||
{/* Status bar */}
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span style={{ marginRight: "0.5em" }}>State: </span>
|
||||
<span
|
||||
style={{
|
||||
marginRight: "0.5em",
|
||||
color: state == WSState.Connected ? "green" : "red",
|
||||
}}
|
||||
>
|
||||
{state}
|
||||
</span>
|
||||
<MatrixWS onStateChange={setState} onMessage={handleMessage} />
|
||||
</div>
|
||||
|
||||
{/* WS messages list */}
|
||||
{messages.map((msg, id) => (
|
||||
<div style={{ margin: "10px", backgroundColor: "black" }}>
|
||||
<JsonView
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { 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 {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
@@ -40,7 +40,7 @@ export function LoginRoute(): React.ReactElement {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onClick={authWithOpenID}
|
||||
startIcon={<Icon path={mdiOpenid} size={1} />}
|
||||
startIcon={<AppIcon src={openid} />}
|
||||
>
|
||||
Sign in with {ServerApi.Config.oidc_provider_name}
|
||||
</Button>
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Message {
|
||||
time_sent: number;
|
||||
time_sent_dayjs: dayjs.Dayjs;
|
||||
modified: boolean;
|
||||
inReplyTo?: string;
|
||||
reactions: Map<string, MessageReaction[]>;
|
||||
content: string;
|
||||
type: MessageType;
|
||||
@@ -37,7 +38,7 @@ export class RoomEventsManager {
|
||||
receiptsEventsMap: Map<string, Receipt[]>;
|
||||
|
||||
get canLoadOlder(): boolean {
|
||||
return !!this.endToken;
|
||||
return !!this.endToken && this.events.length > 0;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -92,13 +93,7 @@ export class RoomEventsManager {
|
||||
content: {
|
||||
body: m.data["m.new_content"]?.body ?? m.data.body,
|
||||
msgtype: m.data.msgtype,
|
||||
"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,
|
||||
"m.relates_to": m.data["m.relates_to"],
|
||||
url: m.data.url,
|
||||
file: m.data.file,
|
||||
},
|
||||
@@ -174,7 +169,7 @@ export class RoomEventsManager {
|
||||
// Message
|
||||
if (data.type === "m.room.message") {
|
||||
// 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(
|
||||
(m) => m.event_id === data.content["m.relates_to"]?.event_id
|
||||
);
|
||||
@@ -206,6 +201,7 @@ export class RoomEventsManager {
|
||||
event_id: evt.id,
|
||||
account: evt.sender,
|
||||
modified: false,
|
||||
inReplyTo: data.content["m.relates_to"]?.["m.in_reply_to"]?.event_id,
|
||||
reactions: new Map(),
|
||||
time_sent: evt.time,
|
||||
time_sent_dayjs: dayjs.unix(evt.time / 1000),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
const State = {
|
||||
Loading: 0,
|
||||
@@ -17,9 +17,7 @@ export function AsyncWidget(p: {
|
||||
ready?: boolean;
|
||||
errAdditionalElement?: () => React.ReactElement;
|
||||
}): React.ReactElement {
|
||||
const [state, setState] = useState<number>(State.Loading);
|
||||
|
||||
const counter = useRef<unknown>(null);
|
||||
const [state, setState] = React.useState<number>(State.Loading);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
@@ -32,12 +30,10 @@ export function AsyncWidget(p: {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (counter.current === p.loadKey) return;
|
||||
counter.current = p.loadKey;
|
||||
|
||||
React.useEffect(() => {
|
||||
load();
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [p.loadKey]);
|
||||
|
||||
if (state === State.Error)
|
||||
return (
|
||||
|
||||
@@ -16,9 +16,16 @@ function emojiUnicode(emoji: string): string {
|
||||
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);
|
||||
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 MuiCard from "@mui/material/Card";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { Outlet } from "react-router";
|
||||
import { AppIcon } from "../../icons/AppIcon";
|
||||
import mdiMessageTextFast from "../../icons/message-text-fast.svg";
|
||||
|
||||
const Card = styled(MuiCard)(({ theme }) => ({
|
||||
display: "flex",
|
||||
@@ -57,12 +57,7 @@ export function BaseLoginPage(): React.ReactElement {
|
||||
variant="h4"
|
||||
sx={{ width: "100%", fontSize: "clamp(2rem, 10vw, 2.15rem)" }}
|
||||
>
|
||||
<Icon
|
||||
path={mdiMessageTextFast}
|
||||
size={"1em"}
|
||||
style={{ display: "inline-table" }}
|
||||
/>{" "}
|
||||
MatrixGW
|
||||
<AppIcon src={mdiMessageTextFast} size={"2em"} /> MatrixGW
|
||||
</Typography>
|
||||
<Outlet />
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { mdiMessageTextFast } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import LogoutIcon from "@mui/icons-material/Logout";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
|
||||
@@ -13,6 +11,8 @@ import Toolbar from "@mui/material/Toolbar";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import * as React from "react";
|
||||
import { AppIcon } from "../../icons/AppIcon";
|
||||
import mdiMessageTextFast from "../../icons/message-text-fast.svg";
|
||||
import { RouterLink } from "../RouterLink";
|
||||
import { useUserInfo } from "./BaseAuthenticatedPage";
|
||||
import ThemeSwitcher from "./ThemeSwitcher";
|
||||
@@ -101,7 +101,7 @@ export default function DashboardHeader({
|
||||
<RouterLink to="/">
|
||||
<Stack direction="row" alignItems="center">
|
||||
<LogoContainer>
|
||||
<Icon path={mdiMessageTextFast} size="2em" />
|
||||
<AppIcon src={mdiMessageTextFast} size="2em" />
|
||||
</LogoContainer>
|
||||
<Typography
|
||||
variant="h6"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { mdiBug, mdiForum, mdiKeyVariant, mdiLinkLock } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import BugReportIcon from "@mui/icons-material/BugReport";
|
||||
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 Drawer from "@mui/material/Drawer";
|
||||
import List from "@mui/material/List";
|
||||
@@ -98,27 +100,27 @@ export default function DashboardSidebar({
|
||||
<DashboardSidebarPageItem
|
||||
disabled={!user.info.matrix_account_connected}
|
||||
title="Messages"
|
||||
icon={<Icon path={mdiForum} size={"1.5em"} />}
|
||||
icon={<ForumIcon style={{ height: "1em", width: "1em" }} />}
|
||||
href="/"
|
||||
mini={viewport === "desktop"}
|
||||
/>
|
||||
<DashboardSidebarDividerItem />
|
||||
<DashboardSidebarPageItem
|
||||
title="Matrix link"
|
||||
icon={<Icon path={mdiLinkLock} size={"1.5em"} />}
|
||||
icon={<LinkIcon style={{ height: "1em", width: "1em" }} />}
|
||||
href="/matrix_link"
|
||||
mini={viewport === "desktop"}
|
||||
/>
|
||||
<DashboardSidebarPageItem
|
||||
title="API tokens"
|
||||
icon={<Icon path={mdiKeyVariant} size={"1.5em"} />}
|
||||
icon={<KeyIcon style={{ height: "1em", width: "1em" }} />}
|
||||
href="/tokens"
|
||||
mini={viewport === "desktop"}
|
||||
/>
|
||||
<DashboardSidebarPageItem
|
||||
disabled={!user.info.matrix_account_connected}
|
||||
title="WS Debug"
|
||||
icon={<Icon path={mdiBug} size={"1.5em"} />}
|
||||
icon={<BugReportIcon style={{ height: "1em", width: "1em" }} />}
|
||||
href="/wsdebug"
|
||||
mini={viewport === "desktop"}
|
||||
/>
|
||||
|
||||
@@ -7,9 +7,10 @@ import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
const theme = useTheme();
|
||||
|
||||
export function useActualColorMode(): {
|
||||
mode: "light" | "dark";
|
||||
setMode: (mode: "light" | "dark") => void;
|
||||
} {
|
||||
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
||||
const preferredMode = prefersDarkMode ? "dark" : "light";
|
||||
|
||||
@@ -17,21 +18,27 @@ export default function ThemeSwitcher() {
|
||||
|
||||
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(() => {
|
||||
setMode(paletteMode === "dark" ? "light" : "dark");
|
||||
}, [setMode, paletteMode]);
|
||||
setMode(mode === "dark" ? "light" : "dark");
|
||||
}, [mode, setMode]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={`${paletteMode === "dark" ? "Light" : "Dark"} mode`}
|
||||
title={`${mode === "dark" ? "Light" : "Dark"} mode`}
|
||||
enterDelay={1000}
|
||||
>
|
||||
<div>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={`Switch to ${
|
||||
paletteMode === "dark" ? "light" : "dark"
|
||||
} mode`}
|
||||
aria-label={`Switch to ${mode === "dark" ? "light" : "dark"} mode`}
|
||||
onClick={toggleMode}
|
||||
>
|
||||
<LightModeIcon
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
type UsersMap,
|
||||
} from "../../api/matrix/MatrixApiProfile";
|
||||
import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom";
|
||||
import {
|
||||
MatrixApiSpace,
|
||||
type SpaceHierarchy,
|
||||
} from "../../api/matrix/MatrixApiSpace";
|
||||
import { MatrixSyncApi } from "../../api/MatrixSyncApi";
|
||||
import type { WsMessage } from "../../api/WsApi";
|
||||
import { RoomEventsManager } from "../../utils/RoomEventsManager";
|
||||
@@ -19,13 +23,19 @@ import { SpaceSelector } from "./SpaceSelector";
|
||||
|
||||
export function MainMessageWidget(): React.ReactElement {
|
||||
const [rooms, setRooms] = React.useState<Room[] | undefined>();
|
||||
const [hierarchy, setHierarchy] = React.useState<
|
||||
SpaceHierarchy | undefined
|
||||
>();
|
||||
const [users, setUsers] = React.useState<UsersMap | undefined>();
|
||||
|
||||
const loadRoomsList = async () => {
|
||||
await MatrixSyncApi.Start();
|
||||
|
||||
const rooms = await MatrixApiRoom.ListJoined();
|
||||
const hierarchy = await MatrixApiSpace.Hierarchy();
|
||||
|
||||
setRooms(rooms);
|
||||
setHierarchy(hierarchy);
|
||||
|
||||
// Get the list of users in rooms
|
||||
const users = rooms.reduce((prev, r) => {
|
||||
@@ -40,11 +50,12 @@ export function MainMessageWidget(): React.ReactElement {
|
||||
<AsyncWidget
|
||||
loadKey={1}
|
||||
load={loadRoomsList}
|
||||
ready={!!rooms && !!users}
|
||||
ready={!!rooms && !!users && !!hierarchy}
|
||||
errMsg="Failed to initialize messaging component!"
|
||||
build={() => (
|
||||
<MainMessageWidgetInner
|
||||
rooms={rooms!}
|
||||
hierarchy={hierarchy!}
|
||||
users={users!}
|
||||
onRoomsListUpdate={(cb) => setRooms((r) => cb(r!))}
|
||||
/>
|
||||
@@ -55,6 +66,7 @@ export function MainMessageWidget(): React.ReactElement {
|
||||
|
||||
function MainMessageWidgetInner(p: {
|
||||
rooms: Room[];
|
||||
hierarchy: SpaceHierarchy;
|
||||
users: UsersMap;
|
||||
onRoomsListUpdate: (cb: (a: Room[]) => Room[]) => void;
|
||||
}): React.ReactElement {
|
||||
@@ -65,11 +77,13 @@ function MainMessageWidgetInner(p: {
|
||||
|
||||
const spaceRooms = React.useMemo(() => {
|
||||
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(
|
||||
(a, b) => (b.latest_event?.time ?? 0) - (a.latest_event?.time ?? 0)
|
||||
);
|
||||
}, [space, p.rooms]);
|
||||
}, [space, p.rooms, p.hierarchy]);
|
||||
|
||||
const unreadRooms = React.useMemo(
|
||||
() =>
|
||||
@@ -111,7 +125,7 @@ function MainMessageWidgetInner(p: {
|
||||
p.onRoomsListUpdate((r) => {
|
||||
const n = [...r];
|
||||
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],
|
||||
number_unread_messages: n[idx].number_unread_messages + 1,
|
||||
|
||||
@@ -55,16 +55,8 @@ export function RoomMessagesList(p: {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
|
||||
}, [lastEventId, messagesEndRef]);
|
||||
|
||||
// Watch scroll to detect when user reach the top to load older messages
|
||||
const handleScroll = async () => {
|
||||
if (!listContainerRef.current || loadingOlder || !p.manager.canLoadOlder)
|
||||
return;
|
||||
|
||||
const { scrollTop } = listContainerRef.current;
|
||||
|
||||
if (scrollTop !== 0) {
|
||||
return;
|
||||
}
|
||||
const loadOlderMessages = async () => {
|
||||
if (loadingOlder || !p.manager.canLoadOlder) return;
|
||||
|
||||
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 (
|
||||
<div
|
||||
onScroll={handleScroll}
|
||||
@@ -94,22 +99,8 @@ export function RoomMessagesList(p: {
|
||||
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 */}
|
||||
{!p.manager.canLoadOlder && (
|
||||
{!p.manager.canLoadOlder && p.manager.messages.length > 0 && (
|
||||
<Typography
|
||||
component={"div"}
|
||||
variant="caption"
|
||||
@@ -119,6 +110,22 @@ export function RoomMessagesList(p: {
|
||||
</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 */}
|
||||
{loadingOlder && (
|
||||
<div
|
||||
@@ -133,6 +140,21 @@ export function RoomMessagesList(p: {
|
||||
</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) => (
|
||||
<RoomMessage
|
||||
key={m.event_id}
|
||||
@@ -149,6 +171,11 @@ export function RoomMessagesList(p: {
|
||||
p.manager.messages[idx - 1].time_sent_dayjs.startOf("day").unix()
|
||||
}
|
||||
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;
|
||||
firstMessageOfDay: boolean;
|
||||
receipts?: Receipt[];
|
||||
repliedMessage?: Message;
|
||||
}): React.ReactElement {
|
||||
const theme = useTheme();
|
||||
const user = useUserInfo();
|
||||
@@ -180,6 +208,8 @@ function RoomMessage(p: {
|
||||
const closeImageFullScreen = () => setShowImageFullScreen(false);
|
||||
|
||||
const sender = p.users.get(p.message.account);
|
||||
const repliedMsgSender =
|
||||
p.repliedMessage && p.users.get(p.repliedMessage.account);
|
||||
|
||||
const handleDeleteMessage = async () => {
|
||||
if (!(await confirm(`Do you really want to delete this message?`))) return;
|
||||
@@ -292,12 +322,37 @@ function RoomMessage(p: {
|
||||
"&:hover *": { visibility: "visible" },
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">
|
||||
{p.message.time_sent_dayjs.format("HH:mm")}
|
||||
<Typography
|
||||
variant="caption"
|
||||
style={{
|
||||
paddingLeft: "2px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{p.message.time_sent_dayjs.format("HH:mm")}
|
||||
</Typography>
|
||||
|
||||
{/** Message itself */}
|
||||
<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 */}
|
||||
{p.message.type === "m.image" && (
|
||||
<img
|
||||
@@ -465,11 +520,9 @@ function RoomMessage(p: {
|
||||
}}
|
||||
>
|
||||
<div style={{ margin: "0px 3px" }}>
|
||||
<EmojiIcon emojiKey={r} />
|
||||
</div>
|
||||
<div style={{ height: "2em", marginLeft: "2px" }}>
|
||||
{reactions.length}
|
||||
<EmojiIcon emojiKey={r} size={16} />
|
||||
</div>
|
||||
<div style={{ marginLeft: "2px" }}>{reactions.length}</div>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import {
|
||||
Chip,
|
||||
List,
|
||||
@@ -5,6 +6,7 @@ import {
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||
@@ -22,11 +24,19 @@ export function RoomSelector(p: {
|
||||
}): React.ReactElement {
|
||||
const user = useUserInfo();
|
||||
|
||||
const [filter, setFilter] = React.useState("");
|
||||
const [unread, setUnread] = React.useState(false);
|
||||
|
||||
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)
|
||||
@@ -45,6 +55,19 @@ export function RoomSelector(p: {
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<div style={{ padding: "5px 10px", marginTop: "5px" }}>
|
||||
<span onClick={() => setUnread(!unread)} style={{ cursor: "pointer" }}>
|
||||
|
||||
@@ -3,17 +3,19 @@ import { Button } from "@mui/material";
|
||||
import React from "react";
|
||||
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||
import type { Room } from "../../api/matrix/MatrixApiRoom";
|
||||
import type { SpaceHierarchy } from "../../api/matrix/MatrixApiSpace";
|
||||
import { RoomIcon } from "./RoomIcon";
|
||||
|
||||
export function SpaceSelector(p: {
|
||||
rooms: Room[];
|
||||
hierarchy: SpaceHierarchy;
|
||||
users: UsersMap;
|
||||
selectedSpace?: string;
|
||||
onChange: (space?: string) => void;
|
||||
}): React.ReactElement {
|
||||
const spaces = React.useMemo(
|
||||
() => p.rooms.filter((r) => r.is_space),
|
||||
[p.rooms]
|
||||
() => p.rooms.filter((r) => r.is_space && p.hierarchy.has(r.id)),
|
||||
[p.rooms, p.hierarchy]
|
||||
);
|
||||
|
||||
// Do not display space bar if your is not member of any space
|
||||
|
||||
Reference in New Issue
Block a user