mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-20 00:43:04 +00:00
feat: matrix
This commit is contained in:
@@ -1,73 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Room } from "matrix-js-sdk";
|
||||
import { matrixClient, currentRoomId } from "./chat";
|
||||
|
||||
let { rooms }: { rooms: Room[] } = $props();
|
||||
</script>
|
||||
|
||||
<div class="rooms">
|
||||
{#each $matrixClient.getRooms() as room}
|
||||
{@const avatar = room.getMxcAvatarUrl()}
|
||||
<button
|
||||
class:active={$currentRoomId === room.roomId}
|
||||
class="room"
|
||||
onclick={() => ($currentRoomId = room.roomId)}
|
||||
>
|
||||
{#if avatar}
|
||||
<img
|
||||
alt={room.name}
|
||||
src={$matrixClient.mxcUrlToHttp(avatar, 16, 16)}
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
{:else}
|
||||
<div>#</div>
|
||||
{/if}
|
||||
<div>{room.name}</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#await $matrixClient.publicRooms()}
|
||||
<div>Loading...</div>
|
||||
{:then rooms}
|
||||
{#each rooms.chunk as room}
|
||||
<button class="room" onclick={() => ($currentRoomId = room.roomId)}>
|
||||
<div>#</div>
|
||||
<div>{room.name}</div>
|
||||
</button>
|
||||
{/each}
|
||||
{:catch error}
|
||||
<div>{error.message}</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.rooms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.room {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
padding-block: 2px;
|
||||
min-height: 0;
|
||||
height: unset;
|
||||
padding-inline: 16px;
|
||||
padding-block: 4px;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
|
||||
&.active {
|
||||
background: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
0
src/lib/chat/MatrixSpace.svelte
Normal file
0
src/lib/chat/MatrixSpace.svelte
Normal file
@@ -1,5 +1,10 @@
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import type { MatrixClient, RoomMember } from "matrix-js-sdk";
|
||||
import { derived, writable, type Writable } from "svelte/store";
|
||||
import type {
|
||||
ClientEvent,
|
||||
LoginResponse,
|
||||
MatrixClient,
|
||||
RoomMember,
|
||||
} from "matrix-js-sdk";
|
||||
import { persistentWritable } from "$lib/storage";
|
||||
import {
|
||||
themeFromSourceColor,
|
||||
@@ -7,14 +12,83 @@ import {
|
||||
type CustomColorGroup,
|
||||
} from "@material/material-color-utilities";
|
||||
import type { UserTheme } from "$lib/preferences";
|
||||
import { MatrixRx } from "./matrix-rx/client";
|
||||
|
||||
export const matrixClient: Writable<MatrixClient> = writable();
|
||||
|
||||
export const isLoggedIn: Writable<boolean> = writable(false);
|
||||
|
||||
export const matrix = derived(
|
||||
[matrixClient, isLoggedIn],
|
||||
([matrixClient, isLoggedIn]) =>
|
||||
isLoggedIn ? new MatrixRx(matrixClient) : undefined,
|
||||
);
|
||||
|
||||
export const currentRoomId = persistentWritable<string | null>(
|
||||
"currentRoomId",
|
||||
null,
|
||||
);
|
||||
|
||||
function getStoredLogin(): LoginResponse | undefined {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem("matrix-login")!);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function storeLogin(response: LoginResponse) {
|
||||
localStorage.setItem("matrix-login", JSON.stringify(response));
|
||||
}
|
||||
|
||||
export async function initMatrixClient() {
|
||||
const { createClient, IndexedDBStore, IndexedDBCryptoStore } = await import(
|
||||
"matrix-js-sdk"
|
||||
);
|
||||
|
||||
const storedLogin = getStoredLogin();
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
dbName: "matrix",
|
||||
indexedDB: window.indexedDB,
|
||||
});
|
||||
const cryptoStore = new IndexedDBCryptoStore(
|
||||
window.indexedDB,
|
||||
"matrix-crypto",
|
||||
);
|
||||
|
||||
const client = createClient({
|
||||
baseUrl: import.meta.env.VITE_MATRIX_URL,
|
||||
userId: storedLogin?.user_id,
|
||||
accessToken: storedLogin?.access_token,
|
||||
timelineSupport: true,
|
||||
store,
|
||||
cryptoStore,
|
||||
});
|
||||
|
||||
console.log("store");
|
||||
await store.startup();
|
||||
console.log("cryptoStore");
|
||||
await cryptoStore.startup();
|
||||
console.log("client");
|
||||
await client.startClient();
|
||||
client.once("sync" as ClientEvent.Sync, () => {
|
||||
isLoggedIn.set(client.isLoggedIn());
|
||||
});
|
||||
|
||||
const loginToken = new URLSearchParams(window.location.search).get(
|
||||
"loginToken",
|
||||
);
|
||||
if (loginToken) {
|
||||
storeLogin(await client.loginWithToken(loginToken));
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
isLoggedIn.set(client.isLoggedIn());
|
||||
}
|
||||
|
||||
matrixClient.set(client);
|
||||
console.log("done");
|
||||
}
|
||||
|
||||
export function memberColor(
|
||||
member: RoomMember,
|
||||
theme: UserTheme,
|
||||
|
||||
71
src/lib/chat/matrix-rx/client.ts
Normal file
71
src/lib/chat/matrix-rx/client.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { Direction, MatrixClient, Room } from "matrix-js-sdk";
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
type Observable,
|
||||
of,
|
||||
distinctUntilChanged,
|
||||
merge,
|
||||
} from "rxjs";
|
||||
import { fromMatrixClientEvent } from "./events";
|
||||
|
||||
function roomListDistinct(prev: Room[], curr: Room[]) {
|
||||
if (prev.length !== curr.length) return false;
|
||||
for (let i = 0; i < prev.length; i++) {
|
||||
if (prev[i]!.roomId !== curr[i]!.roomId) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export class MatrixRx {
|
||||
topLevelRooms$: Observable<Room[]>;
|
||||
|
||||
topLevelSpaces$: Observable<Room[]>;
|
||||
|
||||
topLevelChats$: Observable<Room[]>;
|
||||
|
||||
constructor(private client: MatrixClient) {
|
||||
this.topLevelRooms$ = merge(
|
||||
of([]),
|
||||
fromMatrixClientEvent(client, "Room"),
|
||||
fromMatrixClientEvent(client, "deleteRoom"),
|
||||
fromMatrixClientEvent(client, "Room.myMembership"),
|
||||
fromMatrixClientEvent(client, "Room.CurrentStateUpdated").pipe(
|
||||
filter(
|
||||
([_room, prev, curr]) =>
|
||||
prev.getStateEvents("m.space.parent").length !==
|
||||
curr.getStateEvents("m.space.parent").length,
|
||||
),
|
||||
),
|
||||
).pipe(
|
||||
map(() =>
|
||||
this.client.getVisibleRooms().filter(
|
||||
(room) =>
|
||||
room.getMyMembership() !== "leave" &&
|
||||
room
|
||||
.getLiveTimeline()
|
||||
.getState("f" as Direction.Forward)
|
||||
?.getStateEvents("m.space.parent").length === 0,
|
||||
),
|
||||
),
|
||||
distinctUntilChanged(roomListDistinct),
|
||||
);
|
||||
|
||||
this.topLevelSpaces$ = this.topLevelRooms$.pipe(
|
||||
map((rooms) => rooms.filter((room) => room.isSpaceRoom())),
|
||||
distinctUntilChanged(roomListDistinct),
|
||||
);
|
||||
|
||||
this.topLevelChats$ = this.topLevelRooms$.pipe(
|
||||
map((rooms) => rooms.filter((room) => !room.isSpaceRoom())),
|
||||
distinctUntilChanged(roomListDistinct),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class SpaceRx {
|
||||
constructor(
|
||||
private client: MatrixClient,
|
||||
private space: Room,
|
||||
) {}
|
||||
}
|
||||
11
src/lib/chat/matrix-rx/events.ts
Normal file
11
src/lib/chat/matrix-rx/events.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ClientEventHandlerMap, MatrixClient } from "matrix-js-sdk";
|
||||
import { fromEvent, type Observable } from "rxjs";
|
||||
|
||||
export function fromMatrixClientEvent<T extends keyof ClientEventHandlerMap>(
|
||||
client: MatrixClient,
|
||||
eventName: `${T}`, // hack so we can use strings instead of enums
|
||||
): Observable<Parameters<ClientEventHandlerMap[T]>> {
|
||||
return fromEvent(client, eventName) as Observable<
|
||||
Parameters<ClientEventHandlerMap[T]>
|
||||
>;
|
||||
}
|
||||
85
src/lib/chat/matrix-rx/rooms.ts
Normal file
85
src/lib/chat/matrix-rx/rooms.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
Direction,
|
||||
RoomState,
|
||||
RoomStateEventHandlerMap,
|
||||
EventType,
|
||||
} from "matrix-js-sdk";
|
||||
import { fromMatrixClientEvent } from "./events";
|
||||
import {
|
||||
map,
|
||||
filter,
|
||||
merge,
|
||||
startWith,
|
||||
Observable,
|
||||
of,
|
||||
fromEvent,
|
||||
concat,
|
||||
defer,
|
||||
} from "rxjs";
|
||||
|
||||
export function matrixRoom$(
|
||||
client: MatrixClient,
|
||||
roomId: string | undefined,
|
||||
): Observable<Room | undefined> {
|
||||
return merge([
|
||||
fromMatrixClientEvent(client, "Room").pipe(
|
||||
filter(([room]) => room.roomId === roomId),
|
||||
),
|
||||
fromMatrixClientEvent(client, "deleteRoom").pipe(
|
||||
filter(([id]) => id === roomId),
|
||||
),
|
||||
]).pipe(
|
||||
startWith([]),
|
||||
map(() => client.getRoom(roomId) ?? undefined),
|
||||
);
|
||||
}
|
||||
|
||||
export function roomTimeline$(
|
||||
client: MatrixClient,
|
||||
room: Room | undefined,
|
||||
): Observable<MatrixEvent[] | undefined> {
|
||||
if (!room) return of(undefined);
|
||||
const eventTimeline = room.getLiveTimeline();
|
||||
|
||||
return fromMatrixClientEvent(client, "Room.timeline").pipe(
|
||||
filter(
|
||||
([, eventRoom]) =>
|
||||
eventRoom !== undefined && eventRoom.roomId === room.roomId,
|
||||
),
|
||||
startWith([]),
|
||||
map(() => eventTimeline.getEvents()),
|
||||
);
|
||||
}
|
||||
|
||||
export function roomCurrentStateEvents$(
|
||||
client: MatrixClient,
|
||||
room: Room,
|
||||
eventType: EventType | string,
|
||||
): Observable<MatrixEvent[]> {
|
||||
return concat(
|
||||
defer(() =>
|
||||
of(
|
||||
room
|
||||
.getLiveTimeline()
|
||||
.getState("f" as Direction.Forward)
|
||||
?.getStateEvents(eventType) ?? [],
|
||||
),
|
||||
),
|
||||
fromMatrixClientEvent(client, "Room.CurrentStateUpdated").pipe(
|
||||
filter(([room]) => room.roomId === room.roomId),
|
||||
map(([_room, _prev, curr]) => curr.getStateEvents(eventType)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function fromRoomStateEvent<T extends keyof RoomStateEventHandlerMap>(
|
||||
state: RoomState,
|
||||
eventName: `${T}`,
|
||||
): Observable<Parameters<RoomStateEventHandlerMap[T]>> {
|
||||
return fromEvent(state, eventName) as Observable<
|
||||
Parameters<RoomStateEventHandlerMap[T]>
|
||||
>;
|
||||
}
|
||||
19
src/lib/chat/matrix-rx/timeline.ts
Normal file
19
src/lib/chat/matrix-rx/timeline.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { EventTimeline, MatrixClient, MatrixEvent } from "matrix-js-sdk";
|
||||
import { filter, map, of, startWith, type Observable } from "rxjs";
|
||||
import { fromMatrixClientEvent } from "./events";
|
||||
|
||||
export function roomTimeline(
|
||||
client: MatrixClient,
|
||||
roomId: string | undefined,
|
||||
): Observable<MatrixEvent[]> {
|
||||
if (!roomId) return of([]);
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) return of([]);
|
||||
const eventTimeline = room.getLiveTimeline();
|
||||
|
||||
return fromMatrixClientEvent(client, "Room.timeline").pipe(
|
||||
filter(([, room]) => room?.roomId === roomId),
|
||||
startWith([]),
|
||||
map(() => eventTimeline.getEvents()),
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,17 @@ export function persistentWritable<T>(
|
||||
): Writable<T> {
|
||||
if (browser) {
|
||||
const persistedValue = localStorage.getItem(key);
|
||||
const store =
|
||||
persistedValue !== null
|
||||
? writable(JSON.parse(persistedValue))
|
||||
: writable(value);
|
||||
let store: Writable<T>;
|
||||
try {
|
||||
store =
|
||||
persistedValue !== null
|
||||
? writable(JSON.parse(persistedValue))
|
||||
: writable(value);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
store = writable(value);
|
||||
}
|
||||
store.subscribe((value) => {
|
||||
if (!condition || condition())
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
|
||||
Reference in New Issue
Block a user