Compare commits
10 Commits
bf119a34fb
...
migrate-to
| Author | SHA1 | Date | |
|---|---|---|---|
| 5eab7c3e4f | |||
| a7bfd713c3 | |||
| 4be661d999 | |||
| 1f4e374e66 | |||
| cce9b3de5d | |||
| 820b095be0 | |||
| 0a37688116 | |||
| 4d72644a31 | |||
| 0a395b0d26 | |||
| 639cc6c737 |
@@ -6,6 +6,12 @@ use futures_util::{StreamExt, stream};
|
|||||||
use matrix_sdk::Room;
|
use matrix_sdk::Room;
|
||||||
use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind};
|
use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind};
|
||||||
use matrix_sdk::room::MessagesOptions;
|
use matrix_sdk::room::MessagesOptions;
|
||||||
|
use matrix_sdk::room::edit::EditedContent;
|
||||||
|
use matrix_sdk::ruma::events::reaction::ReactionEventContent;
|
||||||
|
use matrix_sdk::ruma::events::relation::Annotation;
|
||||||
|
use matrix_sdk::ruma::events::room::message::{
|
||||||
|
RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
|
||||||
|
};
|
||||||
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;
|
||||||
@@ -42,7 +48,7 @@ impl APIEvent {
|
|||||||
pub struct APIEventsList {
|
pub struct APIEventsList {
|
||||||
pub start: String,
|
pub start: String,
|
||||||
pub end: Option<String>,
|
pub end: Option<String>,
|
||||||
pub messages: Vec<APIEvent>,
|
pub events: Vec<APIEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get messages for a given room
|
/// Get messages for a given room
|
||||||
@@ -59,7 +65,7 @@ pub(super) async fn get_events(
|
|||||||
Ok(APIEventsList {
|
Ok(APIEventsList {
|
||||||
start: messages.start,
|
start: messages.start,
|
||||||
end: messages.end,
|
end: messages.end,
|
||||||
messages: stream::iter(messages.chunk)
|
events: stream::iter(messages.chunk)
|
||||||
.then(async |msg| APIEvent::from_evt(msg, room.room_id()).await)
|
.then(async |msg| APIEvent::from_evt(msg, room.room_id()).await)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.await
|
.await
|
||||||
@@ -82,10 +88,117 @@ pub async fn get_for_room(
|
|||||||
path: web::Path<RoomIdInPath>,
|
path: web::Path<RoomIdInPath>,
|
||||||
query: web::Query<GetRoomEventsQuery>,
|
query: web::Query<GetRoomEventsQuery>,
|
||||||
) -> HttpResult {
|
) -> HttpResult {
|
||||||
let Some(room) = client.client.client.get_room(&path.id) else {
|
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||||
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.json(get_events(&room, query.limit.unwrap_or(500), query.from.as_deref()).await?))
|
.json(get_events(&room, query.limit.unwrap_or(500), query.from.as_deref()).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SendTextMessageRequest {
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_text_message(
|
||||||
|
client: MatrixClientExtractor,
|
||||||
|
path: web::Path<RoomIdInPath>,
|
||||||
|
) -> HttpResult {
|
||||||
|
let req = client.auth.decode_json_body::<SendTextMessageRequest>()?;
|
||||||
|
|
||||||
|
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||||
|
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
room.send(RoomMessageEventContent::text_plain(req.content))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Accepted().finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct EventIdInPath {
|
||||||
|
pub(crate) event_id: OwnedEventId,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_text_content(
|
||||||
|
client: MatrixClientExtractor,
|
||||||
|
path: web::Path<RoomIdInPath>,
|
||||||
|
event_path: web::Path<EventIdInPath>,
|
||||||
|
) -> HttpResult {
|
||||||
|
let req = client.auth.decode_json_body::<SendTextMessageRequest>()?;
|
||||||
|
|
||||||
|
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||||
|
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let edit_event = match room
|
||||||
|
.make_edit_event(
|
||||||
|
&event_path.event_id,
|
||||||
|
EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain(
|
||||||
|
req.content,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to created edit message event {}: {e}",
|
||||||
|
event_path.event_id
|
||||||
|
);
|
||||||
|
return Ok(HttpResponse::InternalServerError()
|
||||||
|
.json(format!("Failed to create edit message event! {e}")));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(match room.send(edit_event).await {
|
||||||
|
Ok(_) => HttpResponse::Accepted().finish(),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to edit event message {}: {e}", event_path.event_id);
|
||||||
|
HttpResponse::InternalServerError().json(format!("Failed to edit event! {e}"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct EventReactionBody {
|
||||||
|
key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn react_to_event(
|
||||||
|
client: MatrixClientExtractor,
|
||||||
|
path: web::Path<RoomIdInPath>,
|
||||||
|
event_path: web::Path<EventIdInPath>,
|
||||||
|
) -> HttpResult {
|
||||||
|
let body = client.auth.decode_json_body::<EventReactionBody>()?;
|
||||||
|
|
||||||
|
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||||
|
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let annotation = Annotation::new(event_path.event_id.to_owned(), body.key.to_owned());
|
||||||
|
room.send(ReactionEventContent::from(annotation)).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Accepted().finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn redact_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!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(match room.redact(&event_path.event_id, None, None).await {
|
||||||
|
Ok(_) => HttpResponse::Accepted().finish(),
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to redact event {}: {e}", event_path.event_id);
|
||||||
|
HttpResponse::InternalServerError().json(format!("Failed to redact event! {e}"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ impl APIRoomInfo {
|
|||||||
avatar: r.avatar_url(),
|
avatar: r.avatar_url(),
|
||||||
is_space: r.is_space(),
|
is_space: r.is_space(),
|
||||||
parents: parent_spaces,
|
parents: parent_spaces,
|
||||||
number_unread_messages: r.num_unread_messages(),
|
number_unread_messages: r.unread_notification_counts().notification_count,
|
||||||
latest_event: get_events(r, 1, None).await?.messages.into_iter().next(),
|
latest_event: get_events(r, 1, None).await?.events.into_iter().next(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ pub async fn get_joined_spaces(client: MatrixClientExtractor) -> HttpResult {
|
|||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct RoomIdInPath {
|
pub struct RoomIdInPath {
|
||||||
pub(crate) id: OwnedRoomId,
|
pub(crate) room_id: OwnedRoomId,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the list of joined rooms of the user
|
/// Get the list of joined rooms of the user
|
||||||
@@ -91,7 +91,7 @@ pub async fn single_room_info(
|
|||||||
client: MatrixClientExtractor,
|
client: MatrixClientExtractor,
|
||||||
path: web::Path<RoomIdInPath>,
|
path: web::Path<RoomIdInPath>,
|
||||||
) -> HttpResult {
|
) -> HttpResult {
|
||||||
Ok(match client.client.client.get_room(&path.id) {
|
Ok(match client.client.client.get_room(&path.room_id) {
|
||||||
None => HttpResponse::NotFound().json("Room not found"),
|
None => HttpResponse::NotFound().json("Room not found"),
|
||||||
Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r).await?),
|
Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r).await?),
|
||||||
})
|
})
|
||||||
@@ -103,7 +103,7 @@ pub async fn room_avatar(
|
|||||||
client: MatrixClientExtractor,
|
client: MatrixClientExtractor,
|
||||||
path: web::Path<RoomIdInPath>,
|
path: web::Path<RoomIdInPath>,
|
||||||
) -> HttpResult {
|
) -> HttpResult {
|
||||||
let Some(room) = client.client.client.get_room(&path.id) else {
|
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||||
return Ok(HttpResponse::NotFound().json("Room not found"));
|
return Ok(HttpResponse::NotFound().json("Room not found"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -148,11 +148,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
web::get().to(matrix_room_controller::get_joined_spaces),
|
web::get().to(matrix_room_controller::get_joined_spaces),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/matrix/room/{id}",
|
"/api/matrix/room/{room_id}",
|
||||||
web::get().to(matrix_room_controller::single_room_info),
|
web::get().to(matrix_room_controller::single_room_info),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/matrix/room/{id}/avatar",
|
"/api/matrix/room/{room_id}/avatar",
|
||||||
web::get().to(matrix_room_controller::room_avatar),
|
web::get().to(matrix_room_controller::room_avatar),
|
||||||
)
|
)
|
||||||
// Matrix profile controller
|
// Matrix profile controller
|
||||||
@@ -166,9 +166,25 @@ async fn main() -> std::io::Result<()> {
|
|||||||
)
|
)
|
||||||
// Matrix events controller
|
// Matrix events controller
|
||||||
.route(
|
.route(
|
||||||
"/api/matrix/room/{id}/events",
|
"/api/matrix/room/{room_id}/events",
|
||||||
web::get().to(matrix_event_controller::get_for_room),
|
web::get().to(matrix_event_controller::get_for_room),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/matrix/room/{room_id}/send_text_message",
|
||||||
|
web::post().to(matrix_event_controller::send_text_message),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/matrix/room/{room_id}/event/{event_id}/set_text_content",
|
||||||
|
web::post().to(matrix_event_controller::set_text_content),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/matrix/room/{room_id}/event/{event_id}/react",
|
||||||
|
web::post().to(matrix_event_controller::react_to_event),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/matrix/room/{room_id}/event/{event_id}",
|
||||||
|
web::delete().to(matrix_event_controller::redact_event),
|
||||||
|
)
|
||||||
// Matrix media controller
|
// Matrix media controller
|
||||||
.route(
|
.route(
|
||||||
"/api/matrix/media/{mxc}",
|
"/api/matrix/media/{mxc}",
|
||||||
|
|||||||
70
matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts
Normal file
70
matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { APIClient } from "../ApiClient";
|
||||||
|
import type { Room } from "./MatrixApiRoom";
|
||||||
|
|
||||||
|
export interface MatrixRoomMessage {
|
||||||
|
type: "m.room.message";
|
||||||
|
content: {
|
||||||
|
body: string;
|
||||||
|
msgtype: "m.text" | "m.image" | string;
|
||||||
|
"m.relates_to"?: {
|
||||||
|
event_id: string;
|
||||||
|
rel_type: "m.replace" | string;
|
||||||
|
};
|
||||||
|
file?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatrixReaction {
|
||||||
|
type: "m.reaction";
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatrixRoomRedaction {
|
||||||
|
type: "m.room.redaction";
|
||||||
|
redacts: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MatrixEventData =
|
||||||
|
| MatrixRoomMessage
|
||||||
|
| MatrixReaction
|
||||||
|
| MatrixRoomRedaction
|
||||||
|
| { type: "other" };
|
||||||
|
|
||||||
|
export interface MatrixEvent {
|
||||||
|
id: string;
|
||||||
|
time: number;
|
||||||
|
sender: string;
|
||||||
|
data: MatrixEventData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatrixEventsList {
|
||||||
|
start: string;
|
||||||
|
end?: string;
|
||||||
|
events: MatrixEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatrixApiEvent {
|
||||||
|
/**
|
||||||
|
* Get Matrix room events
|
||||||
|
*/
|
||||||
|
static async GetRoomEvents(
|
||||||
|
room: Room,
|
||||||
|
from?: string
|
||||||
|
): Promise<MatrixEventsList> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri:
|
||||||
|
`/matrix/room/${encodeURIComponent(room.id)}/events` +
|
||||||
|
(from ? `?from=${from}` : ""),
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
matrixgw_frontend/src/api/matrix/MatrixApiMedia.ts
Normal file
12
matrixgw_frontend/src/api/matrix/MatrixApiMedia.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { APIClient } from "../ApiClient";
|
||||||
|
|
||||||
|
export class MatrixApiMedia {
|
||||||
|
/**
|
||||||
|
* Get media URL
|
||||||
|
*/
|
||||||
|
static MediaURL(url: string, thumbnail: boolean): string {
|
||||||
|
return `${APIClient.ActualBackendURL()}/matrix/media/${encodeURIComponent(
|
||||||
|
url
|
||||||
|
)}?thumbnail=${thumbnail}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts
Normal file
26
matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { APIClient } from "../ApiClient";
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
user_id: string;
|
||||||
|
display_name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UsersMap = Map<string, UserProfile>;
|
||||||
|
|
||||||
|
export class MatrixApiProfile {
|
||||||
|
/**
|
||||||
|
* Get multiple profiles information
|
||||||
|
*/
|
||||||
|
static async GetMultiple(ids: string[]): Promise<UsersMap> {
|
||||||
|
const list: UserProfile[] = (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "POST",
|
||||||
|
uri: "/matrix/profile/get_multiple",
|
||||||
|
jsonData: ids,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
return new Map(list.map((e) => [e.user_id, e]));
|
||||||
|
}
|
||||||
|
}
|
||||||
55
matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts
Normal file
55
matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { APIClient } from "../ApiClient";
|
||||||
|
import type { UserInfo } from "../AuthApi";
|
||||||
|
import type { MatrixEvent } from "./MatrixApiEvent";
|
||||||
|
import type { UsersMap } from "./MatrixApiProfile";
|
||||||
|
|
||||||
|
export interface Room {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
members: string[];
|
||||||
|
avatar?: string;
|
||||||
|
is_space?: boolean;
|
||||||
|
parents: string[];
|
||||||
|
number_unread_messages: number;
|
||||||
|
latest_event?: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find main member of room
|
||||||
|
*/
|
||||||
|
export function mainRoomMember(user: UserInfo, r: Room): string | undefined {
|
||||||
|
if (r.members.length <= 1) return r.members[0];
|
||||||
|
|
||||||
|
if (r.members.length < 2)
|
||||||
|
return r.members[0] == user.matrix_user_id ? r.members[1] : r.members[0];
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find room name
|
||||||
|
*/
|
||||||
|
export function roomName(user: UserInfo, r: Room, users: UsersMap): string {
|
||||||
|
if (r.name) return r.name;
|
||||||
|
|
||||||
|
const name = r.members
|
||||||
|
.filter((m) => m !== user.matrix_user_id)
|
||||||
|
.map((m) => users.get(m)?.display_name ?? m)
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
return name === "" ? "Empty room" : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatrixApiRoom {
|
||||||
|
/**
|
||||||
|
* Get the list of joined rooms
|
||||||
|
*/
|
||||||
|
static async ListJoined(): Promise<Room[]> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: "/matrix/room/joined",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,3 +7,12 @@ body,
|
|||||||
#root {
|
#root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root > div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { MatrixSyncApi } from "../api/MatrixSyncApi";
|
|
||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
|
||||||
import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage";
|
import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage";
|
||||||
|
import { MainMessageWidget } from "../widgets/messages/MainMessagesWidget";
|
||||||
import { NotLinkedAccountMessage } from "../widgets/NotLinkedAccountMessage";
|
import { NotLinkedAccountMessage } from "../widgets/NotLinkedAccountMessage";
|
||||||
|
|
||||||
export function HomeRoute(): React.ReactElement {
|
export function HomeRoute(): React.ReactElement {
|
||||||
@@ -8,15 +7,5 @@ export function HomeRoute(): React.ReactElement {
|
|||||||
|
|
||||||
if (!user.info.matrix_account_connected) return <NotLinkedAccountMessage />;
|
if (!user.info.matrix_account_connected) return <NotLinkedAccountMessage />;
|
||||||
|
|
||||||
return (
|
return <MainMessageWidget />;
|
||||||
<p>
|
|
||||||
Todo home route{" "}
|
|
||||||
<AsyncWidget
|
|
||||||
loadKey={1}
|
|
||||||
errMsg="Failed to start sync thread!"
|
|
||||||
load={MatrixSyncApi.Start}
|
|
||||||
build={() => <>sync started</>}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
101
matrixgw_frontend/src/utils/RoomEventsManager.ts
Normal file
101
matrixgw_frontend/src/utils/RoomEventsManager.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type {
|
||||||
|
MatrixEvent,
|
||||||
|
MatrixEventsList,
|
||||||
|
} from "../api/matrix/MatrixApiEvent";
|
||||||
|
import type { Room } from "../api/matrix/MatrixApiRoom";
|
||||||
|
|
||||||
|
export interface MessageReaction {
|
||||||
|
event_id: string;
|
||||||
|
account: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
event_id: string;
|
||||||
|
sent: number;
|
||||||
|
modified: boolean;
|
||||||
|
reactions: MessageReaction[];
|
||||||
|
content: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoomEventsManager {
|
||||||
|
readonly room: Room;
|
||||||
|
private events: MatrixEvent[];
|
||||||
|
messages: Message[];
|
||||||
|
endToken?: string;
|
||||||
|
|
||||||
|
constructor(room: Room, initialMessages: MatrixEventsList) {
|
||||||
|
this.room = room;
|
||||||
|
this.events = [];
|
||||||
|
this.messages = [];
|
||||||
|
|
||||||
|
this.processNewEvents(initialMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process events given by the API
|
||||||
|
*/
|
||||||
|
processNewEvents(evts: MatrixEventsList) {
|
||||||
|
this.endToken = evts.end;
|
||||||
|
this.events = [...this.events, ...evts.events];
|
||||||
|
this.rebuildMessagesList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private rebuildMessagesList() {
|
||||||
|
// Sorts events list to process oldest events first
|
||||||
|
this.events.sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
|
// First, process redactions to skip redacted events
|
||||||
|
let redacted = new Set(
|
||||||
|
this.events
|
||||||
|
.map((e) =>
|
||||||
|
e.data.type === "m.room.redaction" ? e.data.redacts : undefined
|
||||||
|
)
|
||||||
|
.filter((e) => e !== undefined)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const evt of this.events) {
|
||||||
|
if (redacted.has(evt.id)) continue;
|
||||||
|
|
||||||
|
const data = evt.data;
|
||||||
|
|
||||||
|
// Message
|
||||||
|
if (data.type === "m.room.message") {
|
||||||
|
// Check if this message replaces another one
|
||||||
|
if (data.content["m.relates_to"]) {
|
||||||
|
const message = this.messages.find(
|
||||||
|
(m) => m.event_id === data.content["m.relates_to"]?.event_id
|
||||||
|
);
|
||||||
|
if (!message) continue;
|
||||||
|
message.modified = true;
|
||||||
|
message.content = data.content.body;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages.push({
|
||||||
|
event_id: evt.id,
|
||||||
|
modified: false,
|
||||||
|
reactions: [],
|
||||||
|
sent: evt.time,
|
||||||
|
image: data.content.file?.url,
|
||||||
|
content: data.content.body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reaction
|
||||||
|
if (data.type === "m.reaction") {
|
||||||
|
const message = this.messages.find(
|
||||||
|
(m) => m.event_id === data.content["m.relates_to"].event_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!message) continue;
|
||||||
|
message.reactions.push({
|
||||||
|
account: evt.sender,
|
||||||
|
event_id: evt.id,
|
||||||
|
key: data.content["m.relates_to"].key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Outlet, useNavigate } from "react-router";
|
import { Outlet, useNavigate } from "react-router";
|
||||||
@@ -105,20 +104,18 @@ export default function BaseAuthenticatedPage(): React.ReactElement {
|
|||||||
signOut,
|
signOut,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<DashboardHeader
|
||||||
|
menuOpen={isNavigationExpanded}
|
||||||
|
onToggleMenu={handleToggleHeaderMenu}
|
||||||
|
/>
|
||||||
<Box
|
<Box
|
||||||
ref={layoutRef}
|
ref={layoutRef}
|
||||||
sx={{
|
sx={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DashboardHeader
|
|
||||||
menuOpen={isNavigationExpanded}
|
|
||||||
onToggleMenu={handleToggleHeaderMenu}
|
|
||||||
/>
|
|
||||||
<DashboardSidebar
|
<DashboardSidebar
|
||||||
expanded={isNavigationExpanded}
|
expanded={isNavigationExpanded}
|
||||||
setExpanded={setIsNavigationExpanded}
|
setExpanded={setIsNavigationExpanded}
|
||||||
@@ -132,7 +129,6 @@ export default function BaseAuthenticatedPage(): React.ReactElement {
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar sx={{ displayPrint: "none" }} />
|
|
||||||
<Box
|
<Box
|
||||||
component="main"
|
component="main"
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -81,7 +81,11 @@ export default function DashboardHeader({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar color="inherit" position="absolute" sx={{ displayPrint: "none" }}>
|
<AppBar
|
||||||
|
color="inherit"
|
||||||
|
position="static"
|
||||||
|
sx={{ displayPrint: "none", overflow: "hidden" }}
|
||||||
|
>
|
||||||
<Toolbar sx={{ backgroundColor: "inherit", mx: { xs: -0.75, sm: -1 } }}>
|
<Toolbar sx={{ backgroundColor: "inherit", mx: { xs: -0.75, sm: -1 } }}>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Icon from "@mdi/react";
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Drawer from "@mui/material/Drawer";
|
import Drawer from "@mui/material/Drawer";
|
||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import type {} from "@mui/material/themeCssVarsAugmentation";
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
@@ -28,7 +27,6 @@ export interface DashboardSidebarProps {
|
|||||||
export default function DashboardSidebar({
|
export default function DashboardSidebar({
|
||||||
expanded = true,
|
expanded = true,
|
||||||
setExpanded,
|
setExpanded,
|
||||||
disableCollapsibleSidebar = false,
|
|
||||||
container,
|
container,
|
||||||
}: DashboardSidebarProps) {
|
}: DashboardSidebarProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -53,8 +51,6 @@ export default function DashboardSidebar({
|
|||||||
return () => {};
|
return () => {};
|
||||||
}, [expanded, theme.transitions.duration.enteringScreen]);
|
}, [expanded, theme.transitions.duration.enteringScreen]);
|
||||||
|
|
||||||
const mini = !disableCollapsibleSidebar && !expanded;
|
|
||||||
|
|
||||||
const handleSetSidebarExpanded = React.useCallback(
|
const handleSetSidebarExpanded = React.useCallback(
|
||||||
(newExpanded: boolean) => () => {
|
(newExpanded: boolean) => () => {
|
||||||
setExpanded(newExpanded);
|
setExpanded(newExpanded);
|
||||||
@@ -66,15 +62,13 @@ export default function DashboardSidebar({
|
|||||||
if (!isOverSmViewport) {
|
if (!isOverSmViewport) {
|
||||||
setExpanded(false);
|
setExpanded(false);
|
||||||
}
|
}
|
||||||
}, [mini, setExpanded, isOverSmViewport]);
|
}, [expanded, setExpanded, isOverSmViewport]);
|
||||||
|
|
||||||
const hasDrawerTransitions =
|
const hasDrawerTransitions = isOverSmViewport && isOverMdViewport;
|
||||||
isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport);
|
|
||||||
|
|
||||||
const getDrawerContent = React.useCallback(
|
const getDrawerContent = React.useCallback(
|
||||||
(viewport: "phone" | "tablet" | "desktop") => (
|
(viewport: "phone" | "tablet" | "desktop") => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Toolbar />
|
|
||||||
<Box
|
<Box
|
||||||
component="nav"
|
component="nav"
|
||||||
aria-label={`${viewport.charAt(0).toUpperCase()}${viewport.slice(1)}`}
|
aria-label={`${viewport.charAt(0).toUpperCase()}${viewport.slice(1)}`}
|
||||||
@@ -84,9 +78,10 @@ export default function DashboardSidebar({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
scrollbarGutter: mini ? "stable" : "auto",
|
scrollbarGutter: !expanded ? "stable" : "auto",
|
||||||
overflowX: "hidden",
|
overflowX: "hidden",
|
||||||
pt: !mini ? 0 : 2,
|
pt: expanded ? 0 : 2,
|
||||||
|
paddingTop: 0,
|
||||||
...(hasDrawerTransitions
|
...(hasDrawerTransitions
|
||||||
? getDrawerSxTransitionMixin(isFullyExpanded, "padding")
|
? getDrawerSxTransitionMixin(isFullyExpanded, "padding")
|
||||||
: {}),
|
: {}),
|
||||||
@@ -95,9 +90,9 @@ export default function DashboardSidebar({
|
|||||||
<List
|
<List
|
||||||
dense
|
dense
|
||||||
sx={{
|
sx={{
|
||||||
padding: mini ? 0 : 0.5,
|
padding: !expanded ? 0 : 0.5,
|
||||||
mb: 4,
|
mb: 4,
|
||||||
width: mini ? MINI_DRAWER_WIDTH : "auto",
|
width: !expanded ? MINI_DRAWER_WIDTH : "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DashboardSidebarPageItem
|
<DashboardSidebarPageItem
|
||||||
@@ -105,30 +100,34 @@ export default function DashboardSidebar({
|
|||||||
title="Messages"
|
title="Messages"
|
||||||
icon={<Icon path={mdiForum} size={"1.5em"} />}
|
icon={<Icon path={mdiForum} size={"1.5em"} />}
|
||||||
href="/"
|
href="/"
|
||||||
|
mini={viewport === "desktop"}
|
||||||
/>
|
/>
|
||||||
<DashboardSidebarDividerItem />
|
<DashboardSidebarDividerItem />
|
||||||
<DashboardSidebarPageItem
|
<DashboardSidebarPageItem
|
||||||
title="Matrix link"
|
title="Matrix link"
|
||||||
icon={<Icon path={mdiLinkLock} size={"1.5em"} />}
|
icon={<Icon path={mdiLinkLock} size={"1.5em"} />}
|
||||||
href="/matrix_link"
|
href="/matrix_link"
|
||||||
|
mini={viewport === "desktop"}
|
||||||
/>
|
/>
|
||||||
<DashboardSidebarPageItem
|
<DashboardSidebarPageItem
|
||||||
title="API tokens"
|
title="API tokens"
|
||||||
icon={<Icon path={mdiKeyVariant} size={"1.5em"} />}
|
icon={<Icon path={mdiKeyVariant} size={"1.5em"} />}
|
||||||
href="/tokens"
|
href="/tokens"
|
||||||
|
mini={viewport === "desktop"}
|
||||||
/>
|
/>
|
||||||
<DashboardSidebarPageItem
|
<DashboardSidebarPageItem
|
||||||
disabled={!user.info.matrix_account_connected}
|
disabled={!user.info.matrix_account_connected}
|
||||||
title="WS Debug"
|
title="WS Debug"
|
||||||
icon={<Icon path={mdiBug} size={"1.5em"} />}
|
icon={<Icon path={mdiBug} size={"1.5em"} />}
|
||||||
href="/wsdebug"
|
href="/wsdebug"
|
||||||
|
mini={viewport === "desktop"}
|
||||||
/>
|
/>
|
||||||
</List>
|
</List>
|
||||||
</Box>
|
</Box>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
mini,
|
expanded,
|
||||||
hasDrawerTransitions,
|
hasDrawerTransitions,
|
||||||
isFullyExpanded,
|
isFullyExpanded,
|
||||||
user.info.matrix_account_connected,
|
user.info.matrix_account_connected,
|
||||||
@@ -136,8 +135,14 @@ export default function DashboardSidebar({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getDrawerSharedSx = React.useCallback(
|
const getDrawerSharedSx = React.useCallback(
|
||||||
(isTemporary: boolean) => {
|
(isTemporary: boolean, desktop?: boolean) => {
|
||||||
const drawerWidth = mini ? MINI_DRAWER_WIDTH : DRAWER_WIDTH;
|
const drawerWidth = desktop
|
||||||
|
? expanded
|
||||||
|
? MINI_DRAWER_WIDTH
|
||||||
|
: 0
|
||||||
|
: !expanded
|
||||||
|
? MINI_DRAWER_WIDTH
|
||||||
|
: DRAWER_WIDTH;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
displayPrint: "none",
|
displayPrint: "none",
|
||||||
@@ -154,17 +159,16 @@ export default function DashboardSidebar({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[expanded, mini]
|
[expanded, !expanded]
|
||||||
);
|
);
|
||||||
|
|
||||||
const sidebarContextValue = React.useMemo(() => {
|
const sidebarContextValue = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
onPageItemClick: handlePageItemClick,
|
onPageItemClick: handlePageItemClick,
|
||||||
mini,
|
|
||||||
fullyExpanded: isFullyExpanded,
|
fullyExpanded: isFullyExpanded,
|
||||||
hasDrawerTransitions,
|
hasDrawerTransitions,
|
||||||
};
|
};
|
||||||
}, [handlePageItemClick, mini, isFullyExpanded, hasDrawerTransitions]);
|
}, [handlePageItemClick, !expanded, isFullyExpanded, hasDrawerTransitions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardSidebarContext.Provider value={sidebarContextValue}>
|
<DashboardSidebarContext.Provider value={sidebarContextValue}>
|
||||||
@@ -179,7 +183,7 @@ export default function DashboardSidebar({
|
|||||||
sx={{
|
sx={{
|
||||||
display: {
|
display: {
|
||||||
xs: "block",
|
xs: "block",
|
||||||
sm: disableCollapsibleSidebar ? "block" : "none",
|
sm: "none",
|
||||||
md: "none",
|
md: "none",
|
||||||
},
|
},
|
||||||
...getDrawerSharedSx(true),
|
...getDrawerSharedSx(true),
|
||||||
@@ -192,7 +196,7 @@ export default function DashboardSidebar({
|
|||||||
sx={{
|
sx={{
|
||||||
display: {
|
display: {
|
||||||
xs: "none",
|
xs: "none",
|
||||||
sm: disableCollapsibleSidebar ? "none" : "block",
|
sm: "block",
|
||||||
md: "none",
|
md: "none",
|
||||||
},
|
},
|
||||||
...getDrawerSharedSx(false),
|
...getDrawerSharedSx(false),
|
||||||
@@ -204,7 +208,7 @@ export default function DashboardSidebar({
|
|||||||
variant="permanent"
|
variant="permanent"
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: "none", md: "block" },
|
display: { xs: "none", md: "block" },
|
||||||
...getDrawerSharedSx(false),
|
...getDrawerSharedSx(false, true),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getDrawerContent("desktop")}
|
{getDrawerContent("desktop")}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import * as React from "react";
|
|||||||
|
|
||||||
const DashboardSidebarContext = React.createContext<{
|
const DashboardSidebarContext = React.createContext<{
|
||||||
onPageItemClick: () => void;
|
onPageItemClick: () => void;
|
||||||
mini: boolean;
|
|
||||||
fullyExpanded: boolean;
|
fullyExpanded: boolean;
|
||||||
hasDrawerTransitions: boolean;
|
hasDrawerTransitions: boolean;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface DashboardSidebarPageItemProps {
|
|||||||
href: string;
|
href: string;
|
||||||
action?: React.ReactNode;
|
action?: React.ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
mini?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardSidebarPageItem({
|
export default function DashboardSidebarPageItem({
|
||||||
@@ -25,6 +26,7 @@ export default function DashboardSidebarPageItem({
|
|||||||
href,
|
href,
|
||||||
action,
|
action,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
mini = false,
|
||||||
}: DashboardSidebarPageItemProps) {
|
}: DashboardSidebarPageItemProps) {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
@@ -32,11 +34,7 @@ export default function DashboardSidebarPageItem({
|
|||||||
if (!sidebarContext) {
|
if (!sidebarContext) {
|
||||||
throw new Error("Sidebar context was used without a provider.");
|
throw new Error("Sidebar context was used without a provider.");
|
||||||
}
|
}
|
||||||
const {
|
const { onPageItemClick, fullyExpanded = true } = sidebarContext;
|
||||||
onPageItemClick,
|
|
||||||
mini = false,
|
|
||||||
fullyExpanded = true,
|
|
||||||
} = sidebarContext;
|
|
||||||
|
|
||||||
const hasExternalHref = href
|
const hasExternalHref = href
|
||||||
? href.startsWith("http://") || href.startsWith("https://")
|
? href.startsWith("http://") || href.startsWith("https://")
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { Divider } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
MatrixApiProfile,
|
||||||
|
type UsersMap,
|
||||||
|
} from "../../api/matrix/MatrixApiProfile";
|
||||||
|
import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom";
|
||||||
|
import { MatrixSyncApi } from "../../api/MatrixSyncApi";
|
||||||
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
|
import { RoomSelector } from "./RoomSelector";
|
||||||
|
import { RoomWidget } from "./RoomWidget";
|
||||||
|
import { SpaceSelector } from "./SpaceSelector";
|
||||||
|
|
||||||
|
export function MainMessageWidget(): React.ReactElement {
|
||||||
|
const [rooms, setRooms] = React.useState<Room[] | undefined>();
|
||||||
|
const [users, setUsers] = React.useState<UsersMap | undefined>();
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
await MatrixSyncApi.Start();
|
||||||
|
|
||||||
|
const rooms = await MatrixApiRoom.ListJoined();
|
||||||
|
setRooms(rooms);
|
||||||
|
|
||||||
|
// Get the list of users in rooms
|
||||||
|
const users = rooms.reduce((prev, r) => {
|
||||||
|
r.members.forEach((m) => prev.add(m));
|
||||||
|
return prev;
|
||||||
|
}, new Set<string>());
|
||||||
|
|
||||||
|
setUsers(await MatrixApiProfile.GetMultiple([...users]));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={1}
|
||||||
|
load={load}
|
||||||
|
ready={!!rooms && !!users}
|
||||||
|
errMsg="Failed to initialize messaging component!"
|
||||||
|
build={() => <_MainMessageWidget rooms={rooms!} users={users!} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _MainMessageWidget(p: {
|
||||||
|
rooms: Room[];
|
||||||
|
users: UsersMap;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [space, setSpace] = React.useState<string | undefined>();
|
||||||
|
const [room, setRoom] = React.useState<Room | undefined>();
|
||||||
|
|
||||||
|
const spaceRooms = React.useMemo(() => {
|
||||||
|
return p.rooms
|
||||||
|
.filter((r) => !r.is_space && (!space || r.parents.includes(space)))
|
||||||
|
.sort(
|
||||||
|
(a, b) => (b.latest_event?.time ?? 0) - (a.latest_event?.time ?? 0)
|
||||||
|
);
|
||||||
|
}, [space, p.rooms]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", height: "100%" }}>
|
||||||
|
<SpaceSelector {...p} selectedSpace={space} onChange={setSpace} />
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<RoomSelector
|
||||||
|
{...p}
|
||||||
|
rooms={spaceRooms}
|
||||||
|
currRoom={room}
|
||||||
|
onChange={setRoom}
|
||||||
|
/>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
{room === undefined && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No room selected.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{room && <RoomWidget {...p} room={room} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
matrixgw_frontend/src/widgets/messages/RoomIcon.tsx
Normal file
33
matrixgw_frontend/src/widgets/messages/RoomIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Avatar } from "@mui/material";
|
||||||
|
import { MatrixApiMedia } from "../../api/matrix/MatrixApiMedia";
|
||||||
|
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||||
|
import {
|
||||||
|
mainRoomMember,
|
||||||
|
roomName,
|
||||||
|
type Room,
|
||||||
|
} from "../../api/matrix/MatrixApiRoom";
|
||||||
|
import { useUserInfo } from "../dashboard/BaseAuthenticatedPage";
|
||||||
|
|
||||||
|
export function RoomIcon(p: {
|
||||||
|
room: Room;
|
||||||
|
users: UsersMap;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const user = useUserInfo();
|
||||||
|
|
||||||
|
let url = p.room.avatar;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
const member = mainRoomMember(user.info, p.room);
|
||||||
|
if (member) url = p.users.get(member)?.avatar;
|
||||||
|
}
|
||||||
|
const name = roomName(user.info, p.room, p.users);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
variant={p.room.is_space ? "square" : undefined}
|
||||||
|
src={url ? MatrixApiMedia.MediaURL(url, true) : undefined}
|
||||||
|
>
|
||||||
|
{name.slice(0, 1)}
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
matrixgw_frontend/src/widgets/messages/RoomSelector.tsx
Normal file
80
matrixgw_frontend/src/widgets/messages/RoomSelector.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
Chip,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
} from "@mui/material";
|
||||||
|
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||||
|
import { roomName, type Room } from "../../api/matrix/MatrixApiRoom";
|
||||||
|
import { useUserInfo } from "../dashboard/BaseAuthenticatedPage";
|
||||||
|
import { RoomIcon } from "./RoomIcon";
|
||||||
|
|
||||||
|
const ROOM_SELECTOR_WIDTH = "300px";
|
||||||
|
|
||||||
|
export function RoomSelector(p: {
|
||||||
|
users: UsersMap;
|
||||||
|
rooms: Room[];
|
||||||
|
currRoom?: Room;
|
||||||
|
onChange: (r: Room) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const user = useUserInfo();
|
||||||
|
|
||||||
|
if (p.rooms.length === 0)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: ROOM_SELECTOR_WIDTH,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No room to display.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
style={{
|
||||||
|
width: ROOM_SELECTOR_WIDTH,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.rooms.map((r) => (
|
||||||
|
<ListItem
|
||||||
|
key={r.id}
|
||||||
|
secondaryAction={
|
||||||
|
r.number_unread_messages === 0 ? undefined : (
|
||||||
|
<Chip color="error" label={r.number_unread_messages} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disablePadding
|
||||||
|
>
|
||||||
|
<ListItemButton
|
||||||
|
role={undefined}
|
||||||
|
onClick={() => p.onChange(r)}
|
||||||
|
dense
|
||||||
|
selected={p.currRoom?.id === r.id}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<RoomIcon room={r} {...p} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontWeight:
|
||||||
|
r.number_unread_messages > 0 ? "bold" : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{roomName(user.info, r, p.users)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
matrixgw_frontend/src/widgets/messages/RoomWidget.tsx
Normal file
30
matrixgw_frontend/src/widgets/messages/RoomWidget.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
|
||||||
|
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||||
|
import type { Room } from "../../api/matrix/MatrixApiRoom";
|
||||||
|
import { RoomEventsManager } from "../../utils/RoomEventsManager";
|
||||||
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
|
|
||||||
|
export function RoomWidget(p: {
|
||||||
|
room: Room;
|
||||||
|
users: UsersMap;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [roomMgr, setRoomMgr] = React.useState<undefined | RoomEventsManager>();
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setRoomMgr(undefined);
|
||||||
|
const messages = await MatrixApiEvent.GetRoomEvents(p.room);
|
||||||
|
const mgr = new RoomEventsManager(p.room, messages);
|
||||||
|
setRoomMgr(mgr);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={p.room.id}
|
||||||
|
ready={!!roomMgr}
|
||||||
|
load={load}
|
||||||
|
errMsg="Failed to load room!"
|
||||||
|
build={() => <>room</>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx
Normal file
53
matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import HomeIcon from "@mui/icons-material/Home";
|
||||||
|
import { Button } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||||
|
import type { Room } from "../../api/matrix/MatrixApiRoom";
|
||||||
|
import { RoomIcon } from "./RoomIcon";
|
||||||
|
|
||||||
|
export function SpaceSelector(p: {
|
||||||
|
rooms: Room[];
|
||||||
|
users: UsersMap;
|
||||||
|
selectedSpace?: string;
|
||||||
|
onChange: (space?: string) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const spaces = React.useMemo(
|
||||||
|
() => p.rooms.filter((r) => r.is_space),
|
||||||
|
[p.rooms]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<SpaceButton
|
||||||
|
icon={<HomeIcon />}
|
||||||
|
onClick={() => p.onChange()}
|
||||||
|
selected={p.selectedSpace === undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{spaces.map((s) => (
|
||||||
|
<SpaceButton
|
||||||
|
key={s.id}
|
||||||
|
icon={<RoomIcon room={s} {...p} />}
|
||||||
|
onClick={() => p.onChange(s.id)}
|
||||||
|
selected={p.selectedSpace === s.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SpaceButton(p: {
|
||||||
|
selected?: boolean;
|
||||||
|
icon: React.ReactElement;
|
||||||
|
onClick: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={p.selected ? "contained" : "text"}
|
||||||
|
style={{ margin: "2px 5px", padding: "25px 10px", fontSize: "200%" }}
|
||||||
|
onClick={p.onClick}
|
||||||
|
>
|
||||||
|
{p.icon}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user