36 Commits

Author SHA1 Message Date
82f80f8ef4 chore(deps): update rust crate clap to 4.6.0
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-20 00:27:16 +00:00
de527d2833 chore: updated project dependencies
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-18 19:59:58 +01:00
e5ca2f98fd Merge branch 'master' of https://gitea.communiquons.org/pierre/MatrixGW 2026-03-18 19:53:19 +01:00
a51edd6093 feat: can get information about connected devices 2026-03-18 19:53:12 +01:00
f544d1d4ca Merge pull request 'chore(deps): update rust crate ractor to 0.15.12' (#208) from renovate/ractor-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-18 00:28:46 +00:00
e950517ab2 Merge pull request 'chore(deps): update rust crate mailchecker to 6.0.20' (#207) from renovate/mailchecker-6.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-18 00:28:41 +00:00
1914d1a429 chore(deps): update rust crate ractor to 0.15.12
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-03-17 00:27:49 +00:00
ea4422701d chore(deps): update rust crate mailchecker to 6.0.20
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-03-17 00:27:47 +00:00
32bbe52cc5 Merge pull request 'chore(deps): update rust crate tokio to 1.50.0' (#206) from renovate/tokio-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-16 00:30:45 +00:00
1cff950f8f Merge pull request 'chore(deps): update rust crate image to 0.25.10' (#205) from renovate/image-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-16 00:30:35 +00:00
a2bdb7e6b8 chore(deps): update rust crate tokio to 1.50.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-03-15 00:30:25 +00:00
3dedd47b14 chore(deps): update rust crate image to 0.25.10
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-03-15 00:30:13 +00:00
ba31e19c76 Merge pull request 'chore(deps): update dependency dayjs to ^1.11.20' (#204) from renovate/dayjs-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-14 00:30:50 +00:00
6557f4ad45 chore(deps): update dependency dayjs to ^1.11.20
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-14 00:30:45 +00:00
18206af6b8 Merge pull request 'chore(deps): update dependency @mui/x-data-grid to ^8.27.5' (#203) from renovate/mui-x-data-grid-8.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-13 00:28:24 +00:00
280388d11f chore(deps): update dependency @mui/x-data-grid to ^8.27.5
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-13 00:28:18 +00:00
7455e8771b Merge pull request 'chore(deps): update materialui to ^7.3.9' (#201) from renovate/materialui into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-12 00:30:30 +00:00
c87dbc670d chore(deps): update materialui to ^7.3.9
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-03-11 00:17:49 +00:00
a5ad5973b7 feat: do not return latest_event info when getting single room information by default
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-10 18:14:49 +01: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
16 changed files with 1267 additions and 675 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,10 @@ edition = "2024"
[dependencies]
env_logger = "0.11.9"
log = "0.4.29"
clap = { version = "4.5.60", features = ["derive", "env"] }
clap = { version = "4.6.0", 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"
@@ -22,18 +22,19 @@ base16ct = { version = "1.0.0", features = ["alloc"] }
futures-util = "0.3.32"
jwt-simple = { version = "0.12.14", default-features = false, features = ["pure-rust"] }
thiserror = "2.0.18"
uuid = { version = "1.21.0", features = ["v4", "serde"] }
ipnet = { version = "2.11.0", features = ["serde"] }
uuid = { version = "1.22.0", features = ["v4", "serde"] }
ipnet = { version = "2.12.0", features = ["serde"] }
rand = "0.10.0"
hex = "0.4.3"
mailchecker = "6.0.19"
mailchecker = "6.0.20"
matrix-sdk = { version = "0.16.0", features = ["e2e-encryption"] }
matrix-sdk-ui = "0.16.0"
url = "2.5.8"
ractor = "0.15.10"
ractor = "0.15.12"
serde_json = "1.0.149"
lazy-regex = "3.6.0"
actix-ws = "0.4.0"
infer = "0.19.0"
rust-embed = "8.11.0"
mime_guess = "2.0.5"
image = "0.25.10"

View File

@@ -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))
}

View File

@@ -14,13 +14,15 @@ 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};
@@ -122,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(
@@ -134,8 +138,15 @@ pub async fn send_text_message(
return Ok(HttpResponse::NotFound().json("Room not found!"));
};
room.send(RoomMessageEventContent::text_plain(req.content))
.await?;
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())
}
@@ -146,9 +157,16 @@ pub struct SendFileForm {
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 {
@@ -182,8 +200,17 @@ pub async fn send_file(
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, AttachmentConfig::new())
room.send_attachment(file_name, mime_type, buff, config)
.await?;
Ok(HttpResponse::Accepted().finish())
@@ -194,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

@@ -115,17 +115,31 @@ pub struct RoomIdInPath {
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
pub async fn single_room_info(
client: MatrixClientExtractor,
path: web::Path<RoomIdInPath>,
query: web::Query<SingleRoomQuery>,
) -> HttpResult {
let notifs = client.client.client.notification_settings().await;
Ok(match client.client.client.get_room(&path.room_id) {
None => HttpResponse::NotFound().json("Room not found"),
Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r, &notifs).await?),
})
let Some(room) = client.client.client.get_room(&path.room_id) else {
return Ok(HttpResponse::NotFound().json("Room not found"));
};
let mut room_info = APIRoomInfo::from_room(&room, &notifs).await?;
if !query.with_latest_event {
room_info.latest_event = None;
}
Ok(HttpResponse::Ok().json(room_info))
}
/// Get room avatar

View File

@@ -1,3 +1,4 @@
pub mod matrix_devices_controller;
pub mod matrix_event_controller;
pub mod matrix_media_controller;
pub mod matrix_profile_controller;

View File

@@ -25,12 +25,16 @@ pub enum HttpFailure {
ActixError(#[from] actix_web::Error),
#[error("Matrix error: {0}")]
MatrixError(#[from] matrix_sdk::Error),
#[error("Matrix HTTP error: {0}")]
MatrixHTTPError(#[from] matrix_sdk::HttpError),
#[error("Matrix decryptor error: {0}")]
MatrixDecryptorError(#[from] matrix_sdk::encryption::DecryptorError),
#[error("Serde JSON error: {0}")]
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

@@ -10,8 +10,8 @@ use matrixgw_backend::app_config::AppConfig;
use matrixgw_backend::broadcast_messages::BroadcastMessage;
use matrixgw_backend::constants;
use matrixgw_backend::controllers::matrix::{
matrix_event_controller, matrix_media_controller, matrix_profile_controller,
matrix_room_controller, matrix_space_controller,
matrix_devices_controller, 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::{
@@ -142,6 +142,11 @@ async fn main() -> std::io::Result<()> {
web::get().to(matrix_sync_thread_controller::status),
)
.service(web::resource("/api/ws").route(web::get().to(ws_controller::ws)))
// Matrix connection status
.route(
"/api/matrix/devices",
web::get().to(matrix_devices_controller::get_list),
)
// Matrix spaces controller
.route(
"/api/matrix/space/hierarchy",
@@ -190,6 +195,10 @@ async fn main() -> std::io::Result<()> {
"/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>

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,12 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/roboto": "^5.2.10",
"@mui/icons-material": "^7.3.8",
"@mui/material": "^7.3.8",
"@mui/x-data-grid": "^8.27.3",
"@mui/icons-material": "^7.3.9",
"@mui/material": "^7.3.9",
"@mui/x-data-grid": "^8.27.5",
"@mui/x-date-pickers": "^8.27.2",
"date-and-time": "^4.3.0",
"dayjs": "^1.11.19",
"date-and-time": "^4.3.1",
"dayjs": "^1.11.20",
"emoji-picker-react": "^4.18.0",
"filesize": "^11.0.13",
"is-cidr": "^6.0.3",
@@ -30,17 +30,17 @@
"react-router": "^7.13.1"
},
"devDependencies": {
"@eslint/js": "^9.39.3",
"@types/node": "^25.3.3",
"@eslint/js": "^10.0.1",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"eslint": "^10.0.2",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.0.3",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
"typescript-eslint": "^8.57.1",
"vite": "npm:rolldown-vite@7.3.1"
},
"overrides": {

View File

@@ -96,23 +96,33 @@ 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): Promise<void> {
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`,
uri:
`/matrix/room/${room.id}/send_file?` +
(inReplyTo ? `in_reply_to=${inReplyTo}` : ""),
formData,
});
}

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,21 +1,38 @@
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, Tooltip } from "@mui/material";
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 { selectFileToUpload } from "../../utils/FilesUtils";
import { ServerApi } from "../../api/ServerApi";
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: SyntheticEvent) => {
e.preventDefault();
@@ -25,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}`);
@@ -36,6 +59,13 @@ 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({
@@ -45,9 +75,15 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
if (!file) return;
loadingMessage.show("Uploading file...");
await MatrixApiEvent.SendFileMessage(p.room, 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}`);
@@ -58,6 +94,31 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
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",
@@ -76,6 +137,12 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
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 />
@@ -90,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>
);
}