Compare commits
41 Commits
2d1ea1bd43
...
renovate/c
| Author | SHA1 | Date | |
|---|---|---|---|
| 82f80f8ef4 | |||
| de527d2833 | |||
| e5ca2f98fd | |||
| a51edd6093 | |||
| f544d1d4ca | |||
| e950517ab2 | |||
| 1914d1a429 | |||
| ea4422701d | |||
| 32bbe52cc5 | |||
| 1cff950f8f | |||
| a2bdb7e6b8 | |||
| 3dedd47b14 | |||
| ba31e19c76 | |||
| 6557f4ad45 | |||
| 18206af6b8 | |||
| 280388d11f | |||
| 7455e8771b | |||
| c87dbc670d | |||
| a5ad5973b7 | |||
| 01b1434e37 | |||
| 7a60460973 | |||
| a8cfdaf287 | |||
| f13fac582b | |||
| 4d9909fe80 | |||
| 13d20ff4fb | |||
| 51d14df6bb | |||
| 9c0af4d7d3 | |||
| 4044a99f3f | |||
| e1e61c4cc5 | |||
| b6ed5f21e9 | |||
| 03fa047014 | |||
| bc09123d52 | |||
| b1b6f66c24 | |||
| cc22293457 | |||
| fbdfbf2b5d | |||
| 68efa064df | |||
| e88961a43a | |||
| f6169d690f | |||
| 5221260e26 | |||
| c562152019 | |||
| 2ea20e6de4 |
850
matrixgw_backend/Cargo.lock
generated
850
matrixgw_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,14 +6,15 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
env_logger = "0.11.9"
|
env_logger = "0.11.9"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
clap = { version = "4.5.59", features = ["derive", "env"] }
|
clap = { version = "4.6.0", features = ["derive", "env"] }
|
||||||
anyhow = "1.0.101"
|
anyhow = "1.0.102"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
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-web = "4.13.0"
|
||||||
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"
|
||||||
|
actix-multipart = "0.7.2"
|
||||||
light-openid = "1.1.0"
|
light-openid = "1.1.0"
|
||||||
bytes = "1.11.1"
|
bytes = "1.11.1"
|
||||||
sha2 = "0.11.0-rc.5"
|
sha2 = "0.11.0-rc.5"
|
||||||
@@ -21,18 +22,19 @@ base16ct = { version = "1.0.0", features = ["alloc"] }
|
|||||||
futures-util = "0.3.32"
|
futures-util = "0.3.32"
|
||||||
jwt-simple = { version = "0.12.14", default-features = false, features = ["pure-rust"] }
|
jwt-simple = { version = "0.12.14", default-features = false, features = ["pure-rust"] }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
uuid = { version = "1.21.0", features = ["v4", "serde"] }
|
uuid = { version = "1.22.0", features = ["v4", "serde"] }
|
||||||
ipnet = { version = "2.11.0", features = ["serde"] }
|
ipnet = { version = "2.12.0", features = ["serde"] }
|
||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
mailchecker = "6.0.19"
|
mailchecker = "6.0.20"
|
||||||
matrix-sdk = { version = "0.16.0", features = ["e2e-encryption"] }
|
matrix-sdk = { version = "0.16.0", features = ["e2e-encryption"] }
|
||||||
matrix-sdk-ui = "0.16.0"
|
matrix-sdk-ui = "0.16.0"
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
ractor = "0.15.10"
|
ractor = "0.15.12"
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
lazy-regex = "3.6.0"
|
lazy-regex = "3.6.0"
|
||||||
actix-ws = "0.3.1"
|
actix-ws = "0.4.0"
|
||||||
infer = "0.19.0"
|
infer = "0.19.0"
|
||||||
rust-embed = "8.11.0"
|
rust-embed = "8.11.0"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
|
image = "0.25.10"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
use crate::controllers::HttpResult;
|
||||||
|
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
/// Get the list of devices for the account
|
||||||
|
pub async fn get_list(client: MatrixClientExtractor) -> HttpResult {
|
||||||
|
let devices = client.client.client.devices().await?.devices;
|
||||||
|
Ok(HttpResponse::Ok().json(devices))
|
||||||
|
}
|
||||||
@@ -2,27 +2,33 @@ use crate::controllers::HttpResult;
|
|||||||
use crate::controllers::matrix::matrix_media_controller;
|
use crate::controllers::matrix::matrix_media_controller;
|
||||||
use crate::controllers::matrix::matrix_media_controller::MediaQuery;
|
use crate::controllers::matrix::matrix_media_controller::MediaQuery;
|
||||||
use crate::controllers::matrix::matrix_room_controller::RoomIdInPath;
|
use crate::controllers::matrix::matrix_room_controller::RoomIdInPath;
|
||||||
|
use crate::controllers::server_controller::ServerConstraints;
|
||||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||||
|
use actix_multipart::form::MultipartForm;
|
||||||
use actix_web::dev::Payload;
|
use actix_web::dev::Payload;
|
||||||
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
|
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
|
||||||
use futures_util::{StreamExt, stream};
|
use futures_util::{StreamExt, stream};
|
||||||
use matrix_sdk::Room;
|
use matrix_sdk::Room;
|
||||||
|
use matrix_sdk::attachment::AttachmentConfig;
|
||||||
use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind};
|
use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind};
|
||||||
use matrix_sdk::media::MediaEventContent;
|
use matrix_sdk::media::MediaEventContent;
|
||||||
use matrix_sdk::room::MessagesOptions;
|
use matrix_sdk::room::MessagesOptions;
|
||||||
use matrix_sdk::room::edit::EditedContent;
|
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::filter::RoomEventFilter;
|
||||||
use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType;
|
use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType;
|
||||||
use matrix_sdk::ruma::events::reaction::ReactionEventContent;
|
use matrix_sdk::ruma::events::reaction::ReactionEventContent;
|
||||||
use matrix_sdk::ruma::events::receipt::ReceiptThread;
|
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::{
|
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::events::{AnyMessageLikeEvent, AnyTimelineEvent};
|
||||||
use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt};
|
use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::value::RawValue;
|
use serde_json::value::RawValue;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct APIEvent {
|
pub struct APIEvent {
|
||||||
@@ -118,6 +124,8 @@ pub async fn get_for_room(
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SendTextMessageRequest {
|
struct SendTextMessageRequest {
|
||||||
content: String,
|
content: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
in_reply_to: Option<OwnedEventId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_text_message(
|
pub async fn send_text_message(
|
||||||
@@ -130,7 +138,79 @@ pub async fn send_text_message(
|
|||||||
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
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?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Accepted().finish())
|
Ok(HttpResponse::Accepted().finish())
|
||||||
@@ -141,6 +221,25 @@ pub struct EventIdInPath {
|
|||||||
pub(crate) event_id: OwnedEventId,
|
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(
|
pub async fn set_text_content(
|
||||||
client: MatrixClientExtractor,
|
client: MatrixClientExtractor,
|
||||||
path: web::Path<RoomIdInPath>,
|
path: web::Path<RoomIdInPath>,
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ use crate::utils::crypt_utils::sha512;
|
|||||||
use actix_web::dev::Payload;
|
use actix_web::dev::Payload;
|
||||||
use actix_web::http::header;
|
use actix_web::http::header;
|
||||||
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
|
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::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
|
||||||
use matrix_sdk::ruma::events::room::MediaSource;
|
use matrix_sdk::ruma::events::room::MediaSource;
|
||||||
use matrix_sdk::ruma::{OwnedMxcUri, UInt};
|
use matrix_sdk::ruma::{OwnedMxcUri, UInt};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct MediaMXCInPath {
|
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
|
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
|
/// Serve a media file
|
||||||
pub async fn serve_media(req: HttpRequest, source: MediaSource, thumbnail: bool) -> HttpResult {
|
pub async fn serve_media(req: HttpRequest, source: MediaSource, thumbnail: bool) -> HttpResult {
|
||||||
let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?;
|
let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?;
|
||||||
|
|
||||||
let media = client
|
let mut media = client
|
||||||
.client
|
.client
|
||||||
.client
|
.client
|
||||||
.media()
|
.media()
|
||||||
@@ -52,11 +88,44 @@ pub async fn serve_media(req: HttpRequest, source: MediaSource, thumbnail: bool)
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let digest = sha512(&media);
|
|
||||||
|
|
||||||
let mime_type = infer::get(&media).map(|x| x.mime_type());
|
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
|
// Check if the browser already knows the etag
|
||||||
|
let digest = sha512(&media);
|
||||||
if let Some(c) = req.headers().get(header::IF_NONE_MATCH)
|
if let Some(c) = req.headers().get(header::IF_NONE_MATCH)
|
||||||
&& c.to_str().unwrap_or("") == digest
|
&& c.to_str().unwrap_or("") == digest
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -115,17 +115,31 @@ pub struct RoomIdInPath {
|
|||||||
pub(crate) room_id: OwnedRoomId,
|
pub(crate) room_id: OwnedRoomId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct SingleRoomQuery {
|
||||||
|
#[serde(default)]
|
||||||
|
pub with_latest_event: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the list of joined rooms of the user
|
/// Get the list of joined rooms of the user
|
||||||
pub async fn single_room_info(
|
pub async fn single_room_info(
|
||||||
client: MatrixClientExtractor,
|
client: MatrixClientExtractor,
|
||||||
path: web::Path<RoomIdInPath>,
|
path: web::Path<RoomIdInPath>,
|
||||||
|
query: web::Query<SingleRoomQuery>,
|
||||||
) -> HttpResult {
|
) -> HttpResult {
|
||||||
let notifs = client.client.client.notification_settings().await;
|
let notifs = client.client.client.notification_settings().await;
|
||||||
|
|
||||||
Ok(match client.client.client.get_room(&path.room_id) {
|
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||||
None => HttpResponse::NotFound().json("Room not found"),
|
return Ok(HttpResponse::NotFound().json("Room not found"));
|
||||||
Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r, ¬ifs).await?),
|
};
|
||||||
})
|
|
||||||
|
let mut room_info = APIRoomInfo::from_room(&room, ¬ifs).await?;
|
||||||
|
|
||||||
|
if !query.with_latest_event {
|
||||||
|
room_info.latest_event = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(room_info))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get room avatar
|
/// Get room avatar
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod matrix_devices_controller;
|
||||||
pub mod matrix_event_controller;
|
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;
|
||||||
|
|||||||
@@ -25,12 +25,16 @@ pub enum HttpFailure {
|
|||||||
ActixError(#[from] actix_web::Error),
|
ActixError(#[from] actix_web::Error),
|
||||||
#[error("Matrix error: {0}")]
|
#[error("Matrix error: {0}")]
|
||||||
MatrixError(#[from] matrix_sdk::Error),
|
MatrixError(#[from] matrix_sdk::Error),
|
||||||
|
#[error("Matrix HTTP error: {0}")]
|
||||||
|
MatrixHTTPError(#[from] matrix_sdk::HttpError),
|
||||||
#[error("Matrix decryptor error: {0}")]
|
#[error("Matrix decryptor error: {0}")]
|
||||||
MatrixDecryptorError(#[from] matrix_sdk::encryption::DecryptorError),
|
MatrixDecryptorError(#[from] matrix_sdk::encryption::DecryptorError),
|
||||||
#[error("Serde JSON error: {0}")]
|
#[error("Serde JSON error: {0}")]
|
||||||
SerdeJSON(#[from] serde_json::Error),
|
SerdeJSON(#[from] serde_json::Error),
|
||||||
#[error("Standard library error: {0}")]
|
#[error("Standard library error: {0}")]
|
||||||
StdLibError(#[from] std::io::Error),
|
StdLibError(#[from] std::io::Error),
|
||||||
|
#[error("Image error: {0}")]
|
||||||
|
ImageDecode(#[from] image::ImageError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for HttpFailure {
|
impl ResponseError for HttpFailure {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub struct ServerConstraints {
|
|||||||
pub token_name: LenConstraints,
|
pub token_name: LenConstraints,
|
||||||
pub token_ip_net: LenConstraints,
|
pub token_ip_net: LenConstraints,
|
||||||
pub token_max_inactivity: LenConstraints,
|
pub token_max_inactivity: LenConstraints,
|
||||||
|
pub max_upload_file_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServerConstraints {
|
impl Default for ServerConstraints {
|
||||||
@@ -47,6 +48,7 @@ impl Default for ServerConstraints {
|
|||||||
token_name: LenConstraints::new(5, 255),
|
token_name: LenConstraints::new(5, 255),
|
||||||
token_ip_net: LenConstraints::max_only(44),
|
token_ip_net: LenConstraints::max_only(44),
|
||||||
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
|
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
|
||||||
|
max_upload_file_size: 20_000_000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ use matrixgw_backend::app_config::AppConfig;
|
|||||||
use matrixgw_backend::broadcast_messages::BroadcastMessage;
|
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_devices_controller, matrix_event_controller, matrix_media_controller,
|
||||||
matrix_room_controller, matrix_space_controller,
|
matrix_profile_controller, matrix_room_controller, matrix_space_controller,
|
||||||
};
|
};
|
||||||
|
use matrixgw_backend::controllers::server_controller::ServerConstraints;
|
||||||
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,
|
||||||
static_controller, tokens_controller, ws_controller,
|
static_controller, tokens_controller, ws_controller,
|
||||||
@@ -75,6 +76,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.wrap(session_mw)
|
.wrap(session_mw)
|
||||||
.wrap(cors)
|
.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(manager_actor_clone.clone()))
|
||||||
.app_data(web::Data::new(RemoteIPConfig {
|
.app_data(web::Data::new(RemoteIPConfig {
|
||||||
proxy: AppConfig::get().proxy_ip.clone(),
|
proxy: AppConfig::get().proxy_ip.clone(),
|
||||||
@@ -138,6 +142,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 connection status
|
||||||
|
.route(
|
||||||
|
"/api/matrix/devices",
|
||||||
|
web::get().to(matrix_devices_controller::get_list),
|
||||||
|
)
|
||||||
// Matrix spaces controller
|
// Matrix spaces controller
|
||||||
.route(
|
.route(
|
||||||
"/api/matrix/space/hierarchy",
|
"/api/matrix/space/hierarchy",
|
||||||
@@ -182,6 +191,14 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/matrix/room/{room_id}/send_text_message",
|
"/api/matrix/room/{room_id}/send_text_message",
|
||||||
web::post().to(matrix_event_controller::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(
|
.route(
|
||||||
"/api/matrix/room/{room_id}/event/{event_id}/set_text_content",
|
"/api/matrix/room/{room_id}/event/{event_id}/set_text_content",
|
||||||
web::post().to(matrix_event_controller::set_text_content),
|
web::post().to(matrix_event_controller::set_text_content),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>MatrixGW</title>
|
<title>MatrixGW</title>
|
||||||
|
<style>body {background-color: black;}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
834
matrixgw_frontend/package-lock.json
generated
834
matrixgw_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,13 +13,14 @@
|
|||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@fontsource/roboto": "^5.2.10",
|
"@fontsource/roboto": "^5.2.10",
|
||||||
"@mui/icons-material": "^7.3.8",
|
"@mui/icons-material": "^7.3.9",
|
||||||
"@mui/material": "^7.3.8",
|
"@mui/material": "^7.3.9",
|
||||||
"@mui/x-data-grid": "^8.27.3",
|
"@mui/x-data-grid": "^8.27.5",
|
||||||
"@mui/x-date-pickers": "^8.27.2",
|
"@mui/x-date-pickers": "^8.27.2",
|
||||||
"date-and-time": "^4.2.0",
|
"date-and-time": "^4.3.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.20",
|
||||||
"emoji-picker-react": "^4.17.4",
|
"emoji-picker-react": "^4.18.0",
|
||||||
|
"filesize": "^11.0.13",
|
||||||
"is-cidr": "^6.0.3",
|
"is-cidr": "^6.0.3",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -29,17 +30,17 @@
|
|||||||
"react-router": "^7.13.1"
|
"react-router": "^7.13.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.3",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.5.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^10.0.3",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^16.5.0",
|
"globals": "^17.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.54.0",
|
"typescript-eslint": "^8.57.1",
|
||||||
"vite": "npm:rolldown-vite@7.3.1"
|
"vite": "npm:rolldown-vite@7.3.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface ServerConstraints {
|
|||||||
token_name: LenConstraint;
|
token_name: LenConstraint;
|
||||||
token_ip_net: LenConstraint;
|
token_ip_net: LenConstraint;
|
||||||
token_max_inactivity: LenConstraint;
|
token_max_inactivity: LenConstraint;
|
||||||
|
max_upload_file_size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LenConstraint {
|
export interface LenConstraint {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export class MatrixApiEvent {
|
|||||||
*/
|
*/
|
||||||
static async GetRoomEvents(
|
static async GetRoomEvents(
|
||||||
room: Room,
|
room: Room,
|
||||||
from?: string
|
from?: string,
|
||||||
): Promise<MatrixEventsList> {
|
): Promise<MatrixEventsList> {
|
||||||
return (
|
return (
|
||||||
await APIClient.exec({
|
await APIClient.exec({
|
||||||
@@ -86,7 +86,7 @@ export class MatrixApiEvent {
|
|||||||
static GetEventFileURL(
|
static GetEventFileURL(
|
||||||
room: Room,
|
room: Room,
|
||||||
event_id: string,
|
event_id: string,
|
||||||
thumbnail: boolean
|
thumbnail: boolean,
|
||||||
): string {
|
): string {
|
||||||
return `${APIClient.ActualBackendURL()}/matrix/room/${
|
return `${APIClient.ActualBackendURL()}/matrix/room/${
|
||||||
room.id
|
room.id
|
||||||
@@ -96,11 +96,34 @@ export class MatrixApiEvent {
|
|||||||
/**
|
/**
|
||||||
* Send text message
|
* 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({
|
await APIClient.exec({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
uri: `/matrix/room/${room.id}/send_text_message`,
|
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(
|
static async SetTextMessageContent(
|
||||||
room: Room,
|
room: Room,
|
||||||
event_id: string,
|
event_id: string,
|
||||||
content: string
|
content: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await APIClient.exec({
|
await APIClient.exec({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -125,7 +148,7 @@ export class MatrixApiEvent {
|
|||||||
static async ReactToEvent(
|
static async ReactToEvent(
|
||||||
room: Room,
|
room: Room,
|
||||||
event_id: string,
|
event_id: string,
|
||||||
key: string
|
key: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await APIClient.exec({
|
await APIClient.exec({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
33
matrixgw_frontend/src/utils/FilesUtils.ts
Normal file
33
matrixgw_frontend/src/utils/FilesUtils.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import AddReactionIcon from "@mui/icons-material/AddReaction";
|
|||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import ReplyIcon from "@mui/icons-material/Reply";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -40,6 +41,7 @@ export function RoomMessagesList(p: {
|
|||||||
room: Room;
|
room: Room;
|
||||||
users: UsersMap;
|
users: UsersMap;
|
||||||
manager: RoomEventsManager;
|
manager: RoomEventsManager;
|
||||||
|
onRequestReplyToMessage: (evt: Message) => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const snackbar = useSnackbar();
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ export function RoomMessagesList(p: {
|
|||||||
try {
|
try {
|
||||||
const older = await MatrixApiEvent.GetRoomEvents(
|
const older = await MatrixApiEvent.GetRoomEvents(
|
||||||
p.room,
|
p.room,
|
||||||
p.manager.endToken
|
p.manager.endToken,
|
||||||
);
|
);
|
||||||
p.manager.processNewEvents(older);
|
p.manager.processNewEvents(older);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -176,6 +178,7 @@ export function RoomMessagesList(p: {
|
|||||||
p.manager.messages.find((s) => s.event_id === m.inReplyTo)) ||
|
p.manager.messages.find((s) => s.event_id === m.inReplyTo)) ||
|
||||||
undefined
|
undefined
|
||||||
}
|
}
|
||||||
|
onRequestReplyToMessage={() => p.onRequestReplyToMessage(m)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -192,6 +195,7 @@ function RoomMessage(p: {
|
|||||||
firstMessageOfDay: boolean;
|
firstMessageOfDay: boolean;
|
||||||
receipts?: Receipt[];
|
receipts?: Receipt[];
|
||||||
repliedMessage?: Message;
|
repliedMessage?: Message;
|
||||||
|
onRequestReplyToMessage: () => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const user = useUserInfo();
|
const user = useUserInfo();
|
||||||
@@ -231,7 +235,7 @@ function RoomMessage(p: {
|
|||||||
await MatrixApiEvent.SetTextMessageContent(
|
await MatrixApiEvent.SetTextMessageContent(
|
||||||
p.room,
|
p.room,
|
||||||
p.message.event_id,
|
p.message.event_id,
|
||||||
editMessage!
|
editMessage!,
|
||||||
);
|
);
|
||||||
setEditMessage(undefined);
|
setEditMessage(undefined);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -259,7 +263,7 @@ function RoomMessage(p: {
|
|||||||
|
|
||||||
const handleToggleReaction = async (
|
const handleToggleReaction = async (
|
||||||
key: string,
|
key: string,
|
||||||
reaction: MessageReaction | undefined
|
reaction: MessageReaction | undefined,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (!reaction)
|
if (!reaction)
|
||||||
@@ -339,7 +343,7 @@ function RoomMessage(p: {
|
|||||||
{p.repliedMessage && repliedMsgSender && (
|
{p.repliedMessage && repliedMsgSender && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "inline-flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
borderLeft: "1px red solid",
|
borderLeft: "1px red solid",
|
||||||
paddingLeft: "10px",
|
paddingLeft: "10px",
|
||||||
@@ -360,7 +364,7 @@ function RoomMessage(p: {
|
|||||||
src={MatrixApiEvent.GetEventFileURL(
|
src={MatrixApiEvent.GetEventFileURL(
|
||||||
p.room,
|
p.room,
|
||||||
p.message.event_id,
|
p.message.event_id,
|
||||||
true
|
true,
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "200px",
|
maxWidth: "200px",
|
||||||
@@ -375,7 +379,7 @@ function RoomMessage(p: {
|
|||||||
src={MatrixApiEvent.GetEventFileURL(
|
src={MatrixApiEvent.GetEventFileURL(
|
||||||
p.room,
|
p.room,
|
||||||
p.message.event_id,
|
p.message.event_id,
|
||||||
false
|
false,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</audio>
|
</audio>
|
||||||
@@ -388,7 +392,7 @@ function RoomMessage(p: {
|
|||||||
src={MatrixApiEvent.GetEventFileURL(
|
src={MatrixApiEvent.GetEventFileURL(
|
||||||
p.room,
|
p.room,
|
||||||
p.message.event_id,
|
p.message.event_id,
|
||||||
false
|
false,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</video>
|
</video>
|
||||||
@@ -400,7 +404,7 @@ function RoomMessage(p: {
|
|||||||
href={MatrixApiEvent.GetEventFileURL(
|
href={MatrixApiEvent.GetEventFileURL(
|
||||||
p.room,
|
p.room,
|
||||||
p.message.event_id,
|
p.message.event_id,
|
||||||
false
|
false,
|
||||||
)}
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
@@ -458,6 +462,10 @@ function RoomMessage(p: {
|
|||||||
<Button onClick={handleAddReaction}>
|
<Button onClick={handleAddReaction}>
|
||||||
<AddReactionIcon />
|
<AddReactionIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
{/* Reply to message */}
|
||||||
|
<Button onClick={p.onRequestReplyToMessage}>
|
||||||
|
<ReplyIcon />
|
||||||
|
</Button>
|
||||||
{/* Edit text message */}
|
{/* Edit text message */}
|
||||||
{p.message.account === user.info.matrix_user_id &&
|
{p.message.account === user.info.matrix_user_id &&
|
||||||
!p.message.file && (
|
!p.message.file && (
|
||||||
@@ -479,7 +487,7 @@ function RoomMessage(p: {
|
|||||||
{[...p.message.reactions.keys()].map((r) => {
|
{[...p.message.reactions.keys()].map((r) => {
|
||||||
const reactions = p.message.reactions.get(r)!;
|
const reactions = p.message.reactions.get(r)!;
|
||||||
const userReaction = reactions.find(
|
const userReaction = reactions.find(
|
||||||
(r) => r.account === user.info.matrix_user_id
|
(r) => r.account === user.info.matrix_user_id,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -537,7 +545,7 @@ function RoomMessage(p: {
|
|||||||
src={MatrixApiEvent.GetEventFileURL(
|
src={MatrixApiEvent.GetEventFileURL(
|
||||||
p.room,
|
p.room,
|
||||||
p.message.event_id,
|
p.message.event_id,
|
||||||
false
|
false,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -607,7 +615,7 @@ function ReactionButton(p: {
|
|||||||
p.message.reactions
|
p.message.reactions
|
||||||
.get(p.emojiKey)
|
.get(p.emojiKey)
|
||||||
?.find(
|
?.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
|
) !== undefined
|
||||||
)
|
)
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
|
|||||||
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 { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider";
|
import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider";
|
||||||
import { RoomEventsManager } from "../../utils/RoomEventsManager";
|
import { type Message, RoomEventsManager } from "../../utils/RoomEventsManager";
|
||||||
import { RoomMessagesList } from "./RoomMessagesList";
|
import { RoomMessagesList } from "./RoomMessagesList";
|
||||||
import { SendMessageForm } from "./SendMessageForm";
|
import { SendMessageForm } from "./SendMessageForm";
|
||||||
import { TypingNotice } from "./TypingNotice";
|
import { TypingNotice } from "./TypingNotice";
|
||||||
@@ -17,6 +17,10 @@ export function RoomWidget(p: {
|
|||||||
|
|
||||||
const receiptId = React.useRef<string | undefined>(undefined);
|
const receiptId = React.useRef<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const [currMessageReply, setCurrMessageReply] = React.useState<
|
||||||
|
Message | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
const handleRoomClick = async () => {
|
const handleRoomClick = async () => {
|
||||||
if (p.manager.messages.length === 0) return;
|
if (p.manager.messages.length === 0) return;
|
||||||
const latest = p.manager.messages[p.manager.messages.length - 1];
|
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 }}
|
style={{ display: "flex", flexDirection: "column", flex: 1 }}
|
||||||
onClick={handleRoomClick}
|
onClick={handleRoomClick}
|
||||||
>
|
>
|
||||||
<RoomMessagesList {...p} />
|
<RoomMessagesList {...p} onRequestReplyToMessage={setCurrMessageReply} />
|
||||||
<TypingNotice {...p} />
|
<TypingNotice {...p} />
|
||||||
<SendMessageForm {...p} />
|
<SendMessageForm
|
||||||
|
{...p}
|
||||||
|
currMessageReply={currMessageReply}
|
||||||
|
setCurrReplyToMessage={setCurrMessageReply}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 SendIcon from "@mui/icons-material/Send";
|
||||||
import { IconButton, TextField } from "@mui/material";
|
import {
|
||||||
import React, { type FormEvent } from "react";
|
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 { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
|
||||||
import type { Room } from "../../api/matrix/MatrixApiRoom";
|
import type { Room } from "../../api/matrix/MatrixApiRoom";
|
||||||
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider";
|
import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider";
|
||||||
import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider";
|
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 loadingMessage = useLoadingMessage();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
const [text, setText] = React.useState("");
|
const [text, setText] = React.useState("");
|
||||||
|
const [pickReaction, setPickReaction] = React.useState(false);
|
||||||
|
|
||||||
const handleTextSubmit = async (e: FormEvent) => {
|
const handleTextSubmit = async (e: SyntheticEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (text === "") return;
|
if (text === "") return;
|
||||||
@@ -20,9 +42,15 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
|
|||||||
loadingMessage.show("Sending message...");
|
loadingMessage.show("Sending message...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await MatrixApiEvent.SendTextMessage(p.room, text);
|
await MatrixApiEvent.SendTextMessage(
|
||||||
|
p.room,
|
||||||
|
text,
|
||||||
|
p.currMessageReply?.event_id,
|
||||||
|
);
|
||||||
|
|
||||||
setText("");
|
setText("");
|
||||||
|
|
||||||
|
p.setCurrReplyToMessage(undefined);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to send message! ${e}`);
|
console.error(`Failed to send message! ${e}`);
|
||||||
alert(`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 (
|
return (
|
||||||
<form onSubmit={handleTextSubmit}>
|
<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
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "10px",
|
padding: "10px",
|
||||||
@@ -50,6 +136,19 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
|
|||||||
value={text}
|
value={text}
|
||||||
onChange={(e) => setText(e.target.value)}
|
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
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
style={{ visibility: text === "" ? "hidden" : "visible" }}
|
style={{ visibility: text === "" ? "hidden" : "visible" }}
|
||||||
@@ -58,6 +157,15 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
|
|||||||
<SendIcon />
|
<SendIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pick reaction dialog */}
|
||||||
|
<Dialog open={pickReaction} onClose={handleCancelAddReaction}>
|
||||||
|
<EmojiPicker
|
||||||
|
emojiStyle={EmojiStyle.NATIVE}
|
||||||
|
theme={Theme.AUTO}
|
||||||
|
onEmojiClick={(emoji) => handleSelectEmoji(emoji.emoji)}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user