23 Commits

Author SHA1 Message Date
13afb22074 chore(deps): update rust crate tokio to 1.50.0
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-10 00:16:45 +00:00
01b1434e37 Merge pull request 'chore(deps): update dependency eslint to ^9.39.4' (#200) from renovate/eslint-9.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-09 00:17:01 +00:00
7a60460973 chore(deps): update dependency eslint to ^9.39.4
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2026-03-09 00:16:59 +00:00
a8cfdaf287 Merge pull request 'chore(deps): update dependency date-and-time to ^4.3.1' (#199) from renovate/date-and-time-4.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-08 00:16:37 +00:00
f13fac582b chore(deps): update dependency date-and-time to ^4.3.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-03-08 00:16:32 +00:00
4d9909fe80 Merge pull request 'chore(deps): update dependency @types/node to ^25.3.5' (#198) from renovate/node-25.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-07 00:20:26 +00:00
13d20ff4fb chore(deps): update dependency @types/node to ^25.3.5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2026-03-07 00:20:21 +00:00
51d14df6bb Merge pull request 'chore(deps): update dependency @mui/x-data-grid to ^8.27.4' (#197) from renovate/mui-x-data-grid-8.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-06 00:17:07 +00:00
9c0af4d7d3 chore(deps): update dependency @mui/x-data-grid to ^8.27.4
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-03-06 00:16:57 +00:00
4044a99f3f feat: can transform media before returning them
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-05 23:04:16 +01:00
e1e61c4cc5 feat: get single event information
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-04 22:04:59 +01:00
b6ed5f21e9 feat: can add emoji when typing message
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-04 21:52:55 +01:00
03fa047014 Fix minor bug with files
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-04 21:47:00 +01:00
bc09123d52 feat: can reply to message 2026-03-04 21:45:50 +01:00
b1b6f66c24 Merge pull request 'chore(deps): update dependency eslint-plugin-react-refresh to ^0.5.2' (#196) from renovate/eslint-plugin-react-refresh-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-04 00:20:17 +00:00
cc22293457 chore(deps): update dependency eslint-plugin-react-refresh to ^0.5.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-03-04 00:20:10 +00:00
fbdfbf2b5d Merge pull request 'chore(deps): update dependency @eslint/js to v10' (#194) from renovate/eslint-js-10.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-03 00:20:04 +00:00
68efa064df chore(deps): update dependency @eslint/js to v10
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-03-03 00:19:59 +00:00
e88961a43a fix: add space around send file button
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-02 23:05:11 +01:00
f6169d690f Remove useless lambda variable
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-02 22:53:50 +01:00
5221260e26 Updated frontend dependencies
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-02 22:53:03 +01:00
c562152019 Merge branch 'master' of https://gitea.communiquons.org/pierre/MatrixGW
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-02 22:49:56 +01:00
2ea20e6de4 feat: can upload files in conversations 2026-03-02 22:49:47 +01:00
16 changed files with 1239 additions and 153 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,14 +6,15 @@ edition = "2024"
[dependencies]
env_logger = "0.11.9"
log = "0.4.29"
clap = { version = "4.5.59", features = ["derive", "env"] }
anyhow = "1.0.101"
clap = { version = "4.5.60", features = ["derive", "env"] }
anyhow = "1.0.102"
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.49.0", features = ["full"] }
tokio = { version = "1.50.0", features = ["full"] }
actix-web = "4.13.0"
actix-session = { version = "0.11.0", features = ["redis-session"] }
actix-remote-ip = "0.1.0"
actix-cors = "0.7.1"
actix-multipart = "0.7.2"
light-openid = "1.1.0"
bytes = "1.11.1"
sha2 = "0.11.0-rc.5"
@@ -32,7 +33,8 @@ url = "2.5.8"
ractor = "0.15.10"
serde_json = "1.0.149"
lazy-regex = "3.6.0"
actix-ws = "0.3.1"
actix-ws = "0.4.0"
infer = "0.19.0"
rust-embed = "8.11.0"
mime_guess = "2.0.5"
mime_guess = "2.0.5"
image = "0.25.9"

View File

@@ -2,27 +2,33 @@ use crate::controllers::HttpResult;
use crate::controllers::matrix::matrix_media_controller;
use crate::controllers::matrix::matrix_media_controller::MediaQuery;
use crate::controllers::matrix::matrix_room_controller::RoomIdInPath;
use crate::controllers::server_controller::ServerConstraints;
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
use actix_multipart::form::MultipartForm;
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
use futures_util::{StreamExt, stream};
use matrix_sdk::Room;
use matrix_sdk::attachment::AttachmentConfig;
use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind};
use matrix_sdk::media::MediaEventContent;
use matrix_sdk::room::MessagesOptions;
use matrix_sdk::room::edit::EditedContent;
use matrix_sdk::room::reply::{EnforceThread, Reply};
use matrix_sdk::ruma::api::client::filter::RoomEventFilter;
use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType;
use matrix_sdk::ruma::events::reaction::ReactionEventContent;
use matrix_sdk::ruma::events::receipt::ReceiptThread;
use matrix_sdk::ruma::events::relation::Annotation;
use matrix_sdk::ruma::events::relation::{Annotation, InReplyTo};
use matrix_sdk::ruma::events::room::message::{
MessageType, RoomMessageEvent, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
MessageType, Relation, RoomMessageEvent, RoomMessageEventContent,
RoomMessageEventContentWithoutRelation,
};
use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent};
use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use std::io::Read;
#[derive(Serialize)]
pub struct APIEvent {
@@ -118,6 +124,8 @@ pub async fn get_for_room(
#[derive(Deserialize)]
struct SendTextMessageRequest {
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
in_reply_to: Option<OwnedEventId>,
}
pub async fn send_text_message(
@@ -130,7 +138,79 @@ pub async fn send_text_message(
return Ok(HttpResponse::NotFound().json("Room not found!"));
};
room.send(RoomMessageEventContent::text_plain(req.content))
let mut evt = RoomMessageEventContent::text_plain(req.content);
if let Some(event_id) = req.in_reply_to {
evt.relates_to = Some(Relation::Reply {
in_reply_to: InReplyTo::new(event_id),
})
};
room.send(evt).await?;
Ok(HttpResponse::Accepted().finish())
}
#[derive(Debug, actix_multipart::form::MultipartForm)]
pub struct SendFileForm {
#[multipart]
file: actix_multipart::form::tempfile::TempFile,
}
#[derive(serde::Deserialize)]
pub struct SendFileQuery {
#[serde(skip_serializing_if = "Option::is_none")]
in_reply_to: Option<OwnedEventId>,
}
pub async fn send_file(
client: MatrixClientExtractor,
path: web::Path<RoomIdInPath>,
query: web::Query<SendFileQuery>,
req: HttpRequest,
) -> HttpResult {
let Some(payload) = client.auth.payload else {
return Ok(HttpResponse::BadRequest().body("No payload included in request!"));
};
// Reconstruct multipart form from authenticated request
let mut form = MultipartForm::<SendFileForm>::from_request(
&req,
&mut Payload::from(bytes::Bytes::from(payload)),
)
.await?;
// Read attachment to end
let mut buff = Vec::with_capacity(form.file.size);
form.file.file.read_to_end(&mut buff)?;
if form.file.size > ServerConstraints::default().max_upload_file_size {
return Ok(HttpResponse::NotFound().json("Uploaded file is too large!"));
}
let Some(room) = client.client.client.get_room(&path.room_id) else {
return Ok(HttpResponse::NotFound().json("Room not found!"));
};
let Some(file_name) = form.file.file_name.as_deref() else {
return Ok(HttpResponse::BadRequest().json("File name must be specified!"));
};
let Some(mime_type) = form.file.content_type.as_ref() else {
return Ok(HttpResponse::BadRequest().json("File content type must be specified!"));
};
let mut config = AttachmentConfig::new();
if let Some(event_id) = query.0.in_reply_to {
config.reply = Some(Reply {
event_id,
enforce_thread: EnforceThread::MaybeThreaded,
})
}
// Do send the file
room.send_attachment(file_name, mime_type, buff, config)
.await?;
Ok(HttpResponse::Accepted().finish())
@@ -141,6 +221,25 @@ pub struct EventIdInPath {
pub(crate) event_id: OwnedEventId,
}
/// Get a single event information
pub async fn get_event(
client: MatrixClientExtractor,
path: web::Path<RoomIdInPath>,
event_path: web::Path<EventIdInPath>,
) -> HttpResult {
let Some(room) = client.client.client.get_room(&path.room_id) else {
return Ok(HttpResponse::NotFound().json("Room not found!"));
};
let event = room.load_or_fetch_event(&event_path.event_id, None).await?;
Ok(match event.kind {
TimelineEventKind::Decrypted(dec) => HttpResponse::Ok().json(dec.event),
TimelineEventKind::UnableToDecrypt { event, .. }
| TimelineEventKind::PlainText { event } => HttpResponse::Ok().json(event),
})
}
pub async fn set_text_content(
client: MatrixClientExtractor,
path: web::Path<RoomIdInPath>,

View File

@@ -4,9 +4,12 @@ use crate::utils::crypt_utils::sha512;
use actix_web::dev::Payload;
use actix_web::http::header;
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
use image::imageops::FilterType;
use image::{ImageFormat, ImageReader};
use matrix_sdk::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
use matrix_sdk::ruma::events::room::MediaSource;
use matrix_sdk::ruma::{OwnedMxcUri, UInt};
use std::io::Cursor;
#[derive(serde::Deserialize)]
pub struct MediaMXCInPath {
@@ -29,11 +32,44 @@ pub async fn serve_mxc_file(req: HttpRequest, media: OwnedMxcUri) -> HttpResult
serve_media(req, MediaSource::Plain(media), query.thumbnail).await
}
#[derive(serde::Deserialize, Default, Copy, Clone)]
#[serde(rename_all(deserialize = "lowercase"))]
enum ConvertFormat {
#[default]
Png,
Jpeg,
Gif,
Bmp,
Tga,
Tiff,
}
#[derive(serde::Deserialize)]
struct ServeMediaQuery {
#[serde(skip_serializing_if = "Option::is_none")]
max_width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
max_height: Option<u32>,
#[serde(default)]
grayscale: bool,
#[serde(skip_serializing_if = "Option::is_none")]
format: Option<ConvertFormat>,
}
impl ServeMediaQuery {
pub fn operation_needed(&self) -> bool {
self.max_width.is_some()
|| self.max_height.is_some()
|| self.format.is_some()
|| self.grayscale
}
}
/// Serve a media file
pub async fn serve_media(req: HttpRequest, source: MediaSource, thumbnail: bool) -> HttpResult {
let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?;
let media = client
let mut media = client
.client
.client
.media()
@@ -52,11 +88,44 @@ pub async fn serve_media(req: HttpRequest, source: MediaSource, thumbnail: bool)
)
.await?;
let digest = sha512(&media);
let mime_type = infer::get(&media).map(|x| x.mime_type());
// Check if the media needs to be converted before being returned
let query = web::Query::<ServeMediaQuery>::from_request(&req, &mut Payload::None).await?;
if query.operation_needed() {
let mut img = ImageReader::new(Cursor::new(&media))
.with_guessed_format()?
.decode()?;
// Resize image if required
if query.max_width.is_some() || query.max_height.is_some() {
img = img.resize(
query.max_width.unwrap_or(img.width()),
query.max_height.unwrap_or(img.height()),
FilterType::Lanczos3,
);
}
// Apply grayscal if needed
if query.grayscale {
img = img.grayscale();
}
// Serialize new image
let format = match query.format.unwrap_or_default() {
ConvertFormat::Png => ImageFormat::Png,
ConvertFormat::Jpeg => ImageFormat::Jpeg,
ConvertFormat::Gif => ImageFormat::Gif,
ConvertFormat::Bmp => ImageFormat::Bmp,
ConvertFormat::Tga => ImageFormat::Tga,
ConvertFormat::Tiff => ImageFormat::Tiff,
};
media.clear();
img.write_to(&mut Cursor::new(&mut media), format)?;
}
// Check if the browser already knows the etag
let digest = sha512(&media);
if let Some(c) = req.headers().get(header::IF_NONE_MATCH)
&& c.to_str().unwrap_or("") == digest
{

View File

@@ -31,6 +31,8 @@ pub enum HttpFailure {
SerdeJSON(#[from] serde_json::Error),
#[error("Standard library error: {0}")]
StdLibError(#[from] std::io::Error),
#[error("Image error: {0}")]
ImageDecode(#[from] image::ImageError),
}
impl ResponseError for HttpFailure {

View File

@@ -39,6 +39,7 @@ pub struct ServerConstraints {
pub token_name: LenConstraints,
pub token_ip_net: LenConstraints,
pub token_max_inactivity: LenConstraints,
pub max_upload_file_size: usize,
}
impl Default for ServerConstraints {
@@ -47,6 +48,7 @@ impl Default for ServerConstraints {
token_name: LenConstraints::new(5, 255),
token_ip_net: LenConstraints::max_only(44),
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
max_upload_file_size: 20_000_000,
}
}
}

View File

@@ -13,6 +13,7 @@ use matrixgw_backend::controllers::matrix::{
matrix_event_controller, matrix_media_controller, matrix_profile_controller,
matrix_room_controller, matrix_space_controller,
};
use matrixgw_backend::controllers::server_controller::ServerConstraints;
use matrixgw_backend::controllers::{
auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller,
static_controller, tokens_controller, ws_controller,
@@ -75,6 +76,9 @@ async fn main() -> std::io::Result<()> {
.wrap(Logger::default())
.wrap(session_mw)
.wrap(cors)
.app_data(web::PayloadConfig::new(
ServerConstraints::default().max_upload_file_size,
))
.app_data(web::Data::new(manager_actor_clone.clone()))
.app_data(web::Data::new(RemoteIPConfig {
proxy: AppConfig::get().proxy_ip.clone(),
@@ -182,6 +186,14 @@ async fn main() -> std::io::Result<()> {
"/api/matrix/room/{room_id}/send_text_message",
web::post().to(matrix_event_controller::send_text_message),
)
.route(
"/api/matrix/room/{room_id}/send_file",
web::post().to(matrix_event_controller::send_file),
)
.route(
"/api/matrix/room/{room_id}/event/{event_id}",
web::get().to(matrix_event_controller::get_event),
)
.route(
"/api/matrix/room/{room_id}/event/{event_id}/set_text_content",
web::post().to(matrix_event_controller::set_text_content),

View File

@@ -5,6 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MatrixGW</title>
<style>body {background-color: black;}</style>
</head>
<body>
<div id="root"></div>

View File

@@ -15,9 +15,10 @@
"@mui/material": "^7.3.8",
"@mui/x-data-grid": "^8.27.3",
"@mui/x-date-pickers": "^8.27.2",
"date-and-time": "^4.2.0",
"date-and-time": "^4.3.0",
"dayjs": "^1.11.19",
"emoji-picker-react": "^4.18.0",
"filesize": "^11.0.13",
"is-cidr": "^6.0.3",
"qrcode.react": "^4.2.0",
"react": "^19.2.4",
@@ -28,16 +29,16 @@
},
"devDependencies": {
"@eslint/js": "^9.39.3",
"@types/node": "^25.1.0",
"@types/node": "^25.3.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"eslint": "^9.39.3",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^16.5.0",
"globals": "^17.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.54.0",
"typescript-eslint": "^8.56.1",
"vite": "npm:rolldown-vite@7.3.1"
}
},
@@ -1495,13 +1496,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz",
"integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==",
"version": "25.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
"integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
"undici-types": "~7.18.0"
}
},
"node_modules/@types/parse-json": {
@@ -1545,17 +1546,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
"integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/type-utils": "8.54.0",
"@typescript-eslint/utils": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0",
"@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/type-utils": "8.56.1",
"@typescript-eslint/utils": "8.56.1",
"@typescript-eslint/visitor-keys": "8.56.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0"
@@ -1568,8 +1569,8 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.54.0",
"eslint": "^8.57.0 || ^9.0.0",
"@typescript-eslint/parser": "^8.56.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
@@ -1584,16 +1585,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0",
"@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1",
"@typescript-eslint/typescript-estree": "8.56.1",
"@typescript-eslint/visitor-keys": "8.56.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1604,19 +1605,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
"integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.54.0",
"@typescript-eslint/types": "^8.54.0",
"@typescript-eslint/tsconfig-utils": "^8.56.1",
"@typescript-eslint/types": "^8.56.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1631,14 +1632,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
"integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0"
"@typescript-eslint/types": "8.56.1",
"@typescript-eslint/visitor-keys": "8.56.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1649,9 +1650,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
"integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1666,15 +1667,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
"integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0",
"@typescript-eslint/utils": "8.54.0",
"@typescript-eslint/types": "8.56.1",
"@typescript-eslint/typescript-estree": "8.56.1",
"@typescript-eslint/utils": "8.56.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
},
@@ -1686,14 +1687,14 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
"integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1705,18 +1706,18 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
"integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.54.0",
"@typescript-eslint/tsconfig-utils": "8.54.0",
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0",
"@typescript-eslint/project-service": "8.56.1",
"@typescript-eslint/tsconfig-utils": "8.56.1",
"@typescript-eslint/types": "8.56.1",
"@typescript-eslint/visitor-keys": "8.56.1",
"debug": "^4.4.3",
"minimatch": "^9.0.5",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.4.0"
@@ -1732,36 +1733,49 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -1772,16 +1786,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
"integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0"
"@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1",
"@typescript-eslint/typescript-estree": "8.56.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1791,19 +1805,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
"integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.54.0",
"eslint-visitor-keys": "^4.2.1"
"@typescript-eslint/types": "8.56.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1813,6 +1827,19 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz",
@@ -2139,9 +2166,9 @@
"license": "MIT"
},
"node_modules/date-and-time": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-4.2.0.tgz",
"integrity": "sha512-bqLrJqsjXMW3yQlR7QrQZqZNVDwo+GkvcAeRzZScAdUAgQPP+baV+j6HFbVp3cpCgVez3Mx2zIPqwk9FMfkVIw==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-4.3.0.tgz",
"integrity": "sha512-dVj2DNzRQtgIrDOcX1DVuMEv0S8wF/zZ6F9OfD1P+zwmtk3hN28nIff8dBGl320DjVNGssfFrdbUad7lDYLZLQ==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -2486,6 +2513,15 @@
"node": ">=16.0.0"
}
},
"node_modules/filesize": {
"version": "11.0.13",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.13.tgz",
"integrity": "sha512-mYJ/qXKvREuO0uH8LTQJ6v7GsUvVOguqxg2VTwQUkyTPXXRRWPdjuUPVqdBrJQhvci48OHlNGRnux+Slr2Rnvw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 10.8.0"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
@@ -2584,9 +2620,9 @@
}
},
"node_modules/globals": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
"integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
"integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3781,16 +3817,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz",
"integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==",
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz",
"integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0",
"@typescript-eslint/utils": "8.54.0"
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@typescript-eslint/typescript-estree": "8.56.1",
"@typescript-eslint/utils": "8.56.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3800,14 +3836,14 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},

View File

@@ -15,11 +15,12 @@
"@fontsource/roboto": "^5.2.10",
"@mui/icons-material": "^7.3.8",
"@mui/material": "^7.3.8",
"@mui/x-data-grid": "^8.27.3",
"@mui/x-data-grid": "^8.27.4",
"@mui/x-date-pickers": "^8.27.2",
"date-and-time": "^4.2.0",
"date-and-time": "^4.3.1",
"dayjs": "^1.11.19",
"emoji-picker-react": "^4.18.0",
"filesize": "^11.0.13",
"is-cidr": "^6.0.3",
"qrcode.react": "^4.2.0",
"react": "^19.2.4",
@@ -29,17 +30,17 @@
"react-router": "^7.13.1"
},
"devDependencies": {
"@eslint/js": "^9.39.3",
"@types/node": "^25.1.0",
"@eslint/js": "^10.0.1",
"@types/node": "^25.3.5",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"eslint": "^9.39.3",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^16.5.0",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.54.0",
"typescript-eslint": "^8.56.1",
"vite": "npm:rolldown-vite@7.3.1"
},
"overrides": {

View File

@@ -16,6 +16,7 @@ export interface ServerConstraints {
token_name: LenConstraint;
token_ip_net: LenConstraint;
token_max_inactivity: LenConstraint;
max_upload_file_size: number;
}
export interface LenConstraint {

View File

@@ -68,7 +68,7 @@ export class MatrixApiEvent {
*/
static async GetRoomEvents(
room: Room,
from?: string
from?: string,
): Promise<MatrixEventsList> {
return (
await APIClient.exec({
@@ -86,7 +86,7 @@ export class MatrixApiEvent {
static GetEventFileURL(
room: Room,
event_id: string,
thumbnail: boolean
thumbnail: boolean,
): string {
return `${APIClient.ActualBackendURL()}/matrix/room/${
room.id
@@ -96,11 +96,34 @@ export class MatrixApiEvent {
/**
* Send text message
*/
static async SendTextMessage(room: Room, content: string): Promise<void> {
static async SendTextMessage(
room: Room,
content: string,
in_reply_to?: string | undefined,
): Promise<void> {
await APIClient.exec({
method: "POST",
uri: `/matrix/room/${room.id}/send_text_message`,
jsonData: { content },
jsonData: { content, in_reply_to },
});
}
/**
* Send file message
*/
static async SendFileMessage(
room: Room,
file: Blob,
inReplyTo?: string | undefined,
): Promise<void> {
const formData = new FormData();
formData.set("file", file);
await APIClient.exec({
method: "POST",
uri:
`/matrix/room/${room.id}/send_file?` +
(inReplyTo ? `in_reply_to=${inReplyTo}` : ""),
formData,
});
}
@@ -110,7 +133,7 @@ export class MatrixApiEvent {
static async SetTextMessageContent(
room: Room,
event_id: string,
content: string
content: string,
): Promise<void> {
await APIClient.exec({
method: "POST",
@@ -125,7 +148,7 @@ export class MatrixApiEvent {
static async ReactToEvent(
room: Room,
event_id: string,
key: string
key: string,
): Promise<void> {
await APIClient.exec({
method: "POST",

View File

@@ -0,0 +1,33 @@
import { filesize } from "filesize";
/**
* Select a file to upload
*/
export async function selectFileToUpload(p: {
allowedTypes?: string[];
maxSize?: number;
}): Promise<Blob | null> {
// Create file element
const fileEl = document.createElement("input");
fileEl.type = "file";
if (p.allowedTypes && p.allowedTypes.length > 0)
fileEl.accept = p.allowedTypes.join(",");
fileEl.click();
// Wait for a file to be chosen
await new Promise((res) =>
fileEl.addEventListener("change", () => res(null)),
);
if ((fileEl.files?.length ?? 0) === 0) return null;
const file = fileEl.files![0];
// Check file size
if (p.maxSize && file.size > p.maxSize) {
throw new Error(
`The file is too big ! (max accepted file size : ${filesize(p.maxSize)})`,
);
}
return file;
}

View File

@@ -2,6 +2,7 @@ import AddReactionIcon from "@mui/icons-material/AddReaction";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
import EditIcon from "@mui/icons-material/Edit";
import ReplyIcon from "@mui/icons-material/Reply";
import {
Box,
Button,
@@ -40,6 +41,7 @@ export function RoomMessagesList(p: {
room: Room;
users: UsersMap;
manager: RoomEventsManager;
onRequestReplyToMessage: (evt: Message) => void;
}): React.ReactElement {
const snackbar = useSnackbar();
@@ -63,7 +65,7 @@ export function RoomMessagesList(p: {
try {
const older = await MatrixApiEvent.GetRoomEvents(
p.room,
p.manager.endToken
p.manager.endToken,
);
p.manager.processNewEvents(older);
} catch (e) {
@@ -176,6 +178,7 @@ export function RoomMessagesList(p: {
p.manager.messages.find((s) => s.event_id === m.inReplyTo)) ||
undefined
}
onRequestReplyToMessage={() => p.onRequestReplyToMessage(m)}
/>
))}
@@ -192,6 +195,7 @@ function RoomMessage(p: {
firstMessageOfDay: boolean;
receipts?: Receipt[];
repliedMessage?: Message;
onRequestReplyToMessage: () => void;
}): React.ReactElement {
const theme = useTheme();
const user = useUserInfo();
@@ -231,7 +235,7 @@ function RoomMessage(p: {
await MatrixApiEvent.SetTextMessageContent(
p.room,
p.message.event_id,
editMessage!
editMessage!,
);
setEditMessage(undefined);
} catch (e) {
@@ -259,7 +263,7 @@ function RoomMessage(p: {
const handleToggleReaction = async (
key: string,
reaction: MessageReaction | undefined
reaction: MessageReaction | undefined,
) => {
try {
if (!reaction)
@@ -339,7 +343,7 @@ function RoomMessage(p: {
{p.repliedMessage && repliedMsgSender && (
<div
style={{
display: "inline-flex",
display: "flex",
alignItems: "center",
borderLeft: "1px red solid",
paddingLeft: "10px",
@@ -360,7 +364,7 @@ function RoomMessage(p: {
src={MatrixApiEvent.GetEventFileURL(
p.room,
p.message.event_id,
true
true,
)}
style={{
maxWidth: "200px",
@@ -375,7 +379,7 @@ function RoomMessage(p: {
src={MatrixApiEvent.GetEventFileURL(
p.room,
p.message.event_id,
false
false,
)}
/>
</audio>
@@ -388,7 +392,7 @@ function RoomMessage(p: {
src={MatrixApiEvent.GetEventFileURL(
p.room,
p.message.event_id,
false
false,
)}
/>
</video>
@@ -400,7 +404,7 @@ function RoomMessage(p: {
href={MatrixApiEvent.GetEventFileURL(
p.room,
p.message.event_id,
false
false,
)}
target="_blank"
rel="noopener"
@@ -458,6 +462,10 @@ function RoomMessage(p: {
<Button onClick={handleAddReaction}>
<AddReactionIcon />
</Button>
{/* Reply to message */}
<Button onClick={p.onRequestReplyToMessage}>
<ReplyIcon />
</Button>
{/* Edit text message */}
{p.message.account === user.info.matrix_user_id &&
!p.message.file && (
@@ -479,7 +487,7 @@ function RoomMessage(p: {
{[...p.message.reactions.keys()].map((r) => {
const reactions = p.message.reactions.get(r)!;
const userReaction = reactions.find(
(r) => r.account === user.info.matrix_user_id
(r) => r.account === user.info.matrix_user_id,
);
return (
<Tooltip
@@ -537,7 +545,7 @@ function RoomMessage(p: {
src={MatrixApiEvent.GetEventFileURL(
p.room,
p.message.event_id,
false
false,
)}
/>
</Dialog>
@@ -607,7 +615,7 @@ function ReactionButton(p: {
p.message.reactions
.get(p.emojiKey)
?.find(
(r) => r.key === p.emojiKey && r.account === user.info.matrix_user_id
(r) => r.key === p.emojiKey && r.account === user.info.matrix_user_id,
) !== undefined
)
return <></>;

View File

@@ -3,7 +3,7 @@ import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
import type { Room } from "../../api/matrix/MatrixApiRoom";
import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider";
import { RoomEventsManager } from "../../utils/RoomEventsManager";
import { type Message, RoomEventsManager } from "../../utils/RoomEventsManager";
import { RoomMessagesList } from "./RoomMessagesList";
import { SendMessageForm } from "./SendMessageForm";
import { TypingNotice } from "./TypingNotice";
@@ -17,6 +17,10 @@ export function RoomWidget(p: {
const receiptId = React.useRef<string | undefined>(undefined);
const [currMessageReply, setCurrMessageReply] = React.useState<
Message | undefined
>();
const handleRoomClick = async () => {
if (p.manager.messages.length === 0) return;
const latest = p.manager.messages[p.manager.messages.length - 1];
@@ -36,9 +40,13 @@ export function RoomWidget(p: {
style={{ display: "flex", flexDirection: "column", flex: 1 }}
onClick={handleRoomClick}
>
<RoomMessagesList {...p} />
<RoomMessagesList {...p} onRequestReplyToMessage={setCurrMessageReply} />
<TypingNotice {...p} />
<SendMessageForm {...p} />
<SendMessageForm
{...p}
currMessageReply={currMessageReply}
setCurrReplyToMessage={setCurrMessageReply}
/>
</div>
);
}

View File

@@ -1,18 +1,40 @@
import AddReactionIcon from "@mui/icons-material/AddReaction";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import CloseIcon from "@mui/icons-material/Close";
import ReplyIcon from "@mui/icons-material/Reply";
import SendIcon from "@mui/icons-material/Send";
import { IconButton, TextField } from "@mui/material";
import React, { type FormEvent } from "react";
import {
Button,
Dialog,
IconButton,
Paper,
TextField,
Tooltip,
} from "@mui/material";
import EmojiPicker, { EmojiStyle, Theme } from "emoji-picker-react";
import React, { type SyntheticEvent } from "react";
import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
import type { Room } from "../../api/matrix/MatrixApiRoom";
import { ServerApi } from "../../api/ServerApi";
import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider";
import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider";
import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider";
import { selectFileToUpload } from "../../utils/FilesUtils";
import type { Message } from "../../utils/RoomEventsManager";
export function SendMessageForm(p: { room: Room }): React.ReactElement {
export function SendMessageForm(p: {
room: Room;
currMessageReply?: Message;
setCurrReplyToMessage: (msg: Message | undefined) => void;
}): React.ReactElement {
const loadingMessage = useLoadingMessage();
const alert = useAlert();
const snackbar = useSnackbar();
const [text, setText] = React.useState("");
const [pickReaction, setPickReaction] = React.useState(false);
const handleTextSubmit = async (e: FormEvent) => {
const handleTextSubmit = async (e: SyntheticEvent) => {
e.preventDefault();
if (text === "") return;
@@ -20,9 +42,15 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
loadingMessage.show("Sending message...");
try {
await MatrixApiEvent.SendTextMessage(p.room, text);
await MatrixApiEvent.SendTextMessage(
p.room,
text,
p.currMessageReply?.event_id,
);
setText("");
p.setCurrReplyToMessage(undefined);
} catch (e) {
console.error(`Failed to send message! ${e}`);
alert(`Failed to send message! ${e}`);
@@ -31,8 +59,66 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
}
};
const handleAddReaction = () => setPickReaction(true);
const handleCancelAddReaction = () => setPickReaction(false);
const handleSelectEmoji = async (key: string) => {
setText((t) => t + key);
setPickReaction(false);
};
const handleFileSubmit = async () => {
try {
const file = await selectFileToUpload({
maxSize: ServerApi.Config.constraints.max_upload_file_size,
});
if (!file) return;
loadingMessage.show("Uploading file...");
await MatrixApiEvent.SendFileMessage(
p.room,
file,
p.currMessageReply?.event_id,
);
snackbar("The file was successfully uploaded!");
p.setCurrReplyToMessage(undefined);
} catch (e) {
console.error(e);
alert(`Failed to upload file! ${e}`);
} finally {
loadingMessage.hide();
}
};
return (
<form onSubmit={handleTextSubmit}>
{/* Show replied message content */}
{p.currMessageReply && (
<Paper
variant="outlined"
style={{
display: "flex",
padding: "5px 10px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<ReplyIcon />
<span style={{ flex: 1, marginLeft: "10px" }}>
{p.currMessageReply.content}
</span>
<Button
size="large"
onClick={() => p.setCurrReplyToMessage(undefined)}
>
<CloseIcon fontSize="inherit" />
</Button>
</Paper>
)}
{/* Input form */}
<div
style={{
padding: "10px",
@@ -50,6 +136,19 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
value={text}
onChange={(e) => setText(e.target.value)}
/>
<span style={{ width: "10px" }}></span>
<Tooltip title="Add a reaction">
<IconButton size="small" onClick={handleAddReaction}>
<AddReactionIcon />
</IconButton>
</Tooltip>
<span style={{ width: "10px" }}></span>
<Tooltip title="Send a file">
<IconButton size="small" onClick={handleFileSubmit}>
<AttachFileIcon />
</IconButton>
</Tooltip>
<span style={{ width: "10px" }}></span>
<IconButton
size="small"
style={{ visibility: text === "" ? "hidden" : "visible" }}
@@ -58,6 +157,15 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
<SendIcon />
</IconButton>
</div>
{/* Pick reaction dialog */}
<Dialog open={pickReaction} onClose={handleCancelAddReaction}>
<EmojiPicker
emojiStyle={EmojiStyle.NATIVE}
theme={Theme.AUTO}
onEmojiClick={(emoji) => handleSelectEmoji(emoji.emoji)}
/>
</Dialog>
</form>
);
}