mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-20 08:52:59 +00:00
feat: matrix
This commit is contained in:
@@ -69,6 +69,7 @@
|
|||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"stylelint": "^16.8.1",
|
"stylelint": "^16.8.1",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -113,6 +113,9 @@ importers:
|
|||||||
prettier-plugin-svelte:
|
prettier-plugin-svelte:
|
||||||
specifier: ^3.2.6
|
specifier: ^3.2.6
|
||||||
version: 3.2.6(prettier@3.3.3)(svelte@5.0.0-next.221)
|
version: 3.2.6(prettier@3.3.3)(svelte@5.0.0-next.221)
|
||||||
|
rxjs:
|
||||||
|
specifier: ^7.8.1
|
||||||
|
version: 7.8.1
|
||||||
sass:
|
sass:
|
||||||
specifier: ^1.77.8
|
specifier: ^1.77.8
|
||||||
version: 1.77.8
|
version: 1.77.8
|
||||||
|
|||||||
@@ -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 { derived, writable, type Writable } from "svelte/store";
|
||||||
import type { MatrixClient, RoomMember } from "matrix-js-sdk";
|
import type {
|
||||||
|
ClientEvent,
|
||||||
|
LoginResponse,
|
||||||
|
MatrixClient,
|
||||||
|
RoomMember,
|
||||||
|
} from "matrix-js-sdk";
|
||||||
import { persistentWritable } from "$lib/storage";
|
import { persistentWritable } from "$lib/storage";
|
||||||
import {
|
import {
|
||||||
themeFromSourceColor,
|
themeFromSourceColor,
|
||||||
@@ -7,14 +12,83 @@ import {
|
|||||||
type CustomColorGroup,
|
type CustomColorGroup,
|
||||||
} from "@material/material-color-utilities";
|
} from "@material/material-color-utilities";
|
||||||
import type { UserTheme } from "$lib/preferences";
|
import type { UserTheme } from "$lib/preferences";
|
||||||
|
import { MatrixRx } from "./matrix-rx/client";
|
||||||
|
|
||||||
export const matrixClient: Writable<MatrixClient> = writable();
|
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>(
|
export const currentRoomId = persistentWritable<string | null>(
|
||||||
"currentRoomId",
|
"currentRoomId",
|
||||||
null,
|
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(
|
export function memberColor(
|
||||||
member: RoomMember,
|
member: RoomMember,
|
||||||
theme: UserTheme,
|
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> {
|
): Writable<T> {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const persistedValue = localStorage.getItem(key);
|
const persistedValue = localStorage.getItem(key);
|
||||||
const store =
|
let store: Writable<T>;
|
||||||
persistedValue !== null
|
try {
|
||||||
? writable(JSON.parse(persistedValue))
|
store =
|
||||||
: writable(value);
|
persistedValue !== null
|
||||||
|
? writable(JSON.parse(persistedValue))
|
||||||
|
: writable(value);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
store = writable(value);
|
||||||
|
}
|
||||||
store.subscribe((value) => {
|
store.subscribe((value) => {
|
||||||
if (!condition || condition())
|
if (!condition || condition())
|
||||||
localStorage.setItem(key, JSON.stringify(value));
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
import { initSerial } from "$lib/serial/connection";
|
import { initSerial } from "$lib/serial/connection";
|
||||||
import type { LayoutData } from "./$types";
|
import type { LayoutData } from "./$types";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import BrowserWarning from "./BrowserWarning.svelte";
|
|
||||||
import "tippy.js/animations/shift-away.css";
|
import "tippy.js/animations/shift-away.css";
|
||||||
import "tippy.js/dist/tippy.css";
|
import "tippy.js/dist/tippy.css";
|
||||||
import tippy from "tippy.js";
|
import tippy from "tippy.js";
|
||||||
@@ -30,6 +29,7 @@
|
|||||||
import { restoreFromFile } from "$lib/backup/backup";
|
import { restoreFromFile } from "$lib/backup/backup";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { hotkeys } from "$lib/title";
|
import { hotkeys } from "$lib/title";
|
||||||
|
import { initMatrixClient } from "$lib/chat/chat";
|
||||||
|
|
||||||
const locale =
|
const locale =
|
||||||
((browser && localStorage.getItem("locale")) as Locales) || detectLocale();
|
((browser && localStorage.getItem("locale")) as Locales) || detectLocale();
|
||||||
@@ -66,6 +66,9 @@
|
|||||||
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
|
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
|
||||||
await initSerial();
|
await initSerial();
|
||||||
}
|
}
|
||||||
|
if (browser) {
|
||||||
|
await initMatrixClient();
|
||||||
|
}
|
||||||
|
|
||||||
if (data.importFile) {
|
if (data.importFile) {
|
||||||
restoreFromFile(data.importFile);
|
restoreFromFile(data.importFile);
|
||||||
@@ -128,10 +131,6 @@
|
|||||||
</PageTransition>
|
</PageTransition>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
{#if import.meta.env.TAURI_FAMILY === undefined && browser && !("serial" in navigator)}
|
|
||||||
<BrowserWarning />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -1,184 +1,84 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from "$app/environment";
|
import { isLoggedIn, matrix } from "$lib/chat/chat";
|
||||||
import { onDestroy, onMount, setContext } from "svelte";
|
import { flip } from "svelte/animate";
|
||||||
import type {
|
import { slide } from "svelte/transition";
|
||||||
IndexedDBStore,
|
import Login from "./Login.svelte";
|
||||||
IndexedDBCryptoStore,
|
|
||||||
LoginResponse,
|
|
||||||
} from "matrix-js-sdk";
|
|
||||||
import MatrixTimeline from "$lib/chat/MatrixTimeline.svelte";
|
|
||||||
import { matrixClient, currentRoomId } from "$lib/chat/chat";
|
|
||||||
import MatrixRooms from "$lib/chat/MatrixRooms.svelte";
|
|
||||||
import MatrixRoomMembers from "$lib/chat/MatrixRoomMembers.svelte";
|
|
||||||
|
|
||||||
let loggedIn = $state(false);
|
let { children } = $props();
|
||||||
let ready = $state(false);
|
|
||||||
|
|
||||||
let store: IndexedDBStore;
|
let spaces = $derived($matrix?.topLevelSpaces$);
|
||||||
let cryptoStore: IndexedDBCryptoStore;
|
|
||||||
|
|
||||||
onMount(async () => {
|
function spaceShort(name: string) {
|
||||||
if (!browser) return;
|
return name
|
||||||
const { createClient, IndexedDBStore, IndexedDBCryptoStore } = await import(
|
.split(" ")
|
||||||
"matrix-js-sdk"
|
.map((it) => it[0])
|
||||||
);
|
.join("");
|
||||||
|
|
||||||
const storedLogin = getStoredLogin();
|
|
||||||
|
|
||||||
store = new IndexedDBStore({
|
|
||||||
dbName: "matrix",
|
|
||||||
indexedDB: window.indexedDB,
|
|
||||||
});
|
|
||||||
cryptoStore = new IndexedDBCryptoStore(window.indexedDB, "matrix-crypto");
|
|
||||||
|
|
||||||
$matrixClient = createClient({
|
|
||||||
baseUrl: import.meta.env.VITE_MATRIX_URL,
|
|
||||||
userId: storedLogin?.user_id,
|
|
||||||
accessToken: storedLogin?.access_token,
|
|
||||||
timelineSupport: true,
|
|
||||||
store,
|
|
||||||
cryptoStore,
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginToken = new URLSearchParams(window.location.search).get(
|
|
||||||
"loginToken",
|
|
||||||
);
|
|
||||||
if (loginToken) {
|
|
||||||
await handleLogin(await $matrixClient.loginWithToken(loginToken));
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
await postLogin();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function passwordLogin(event: SubmitEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
const form = event.target as HTMLFormElement;
|
|
||||||
const username = (form.elements.namedItem("username") as HTMLInputElement)
|
|
||||||
.value;
|
|
||||||
const password = (form.elements.namedItem("password") as HTMLInputElement)
|
|
||||||
.value;
|
|
||||||
|
|
||||||
await handleLogin(
|
|
||||||
await $matrixClient.loginWithPassword(username, password),
|
|
||||||
);
|
|
||||||
await postLogin();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogin(response: LoginResponse) {
|
|
||||||
localStorage.setItem("matrix-login", JSON.stringify(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postLogin() {
|
|
||||||
loggedIn = $matrixClient.isLoggedIn();
|
|
||||||
|
|
||||||
if (loggedIn) {
|
|
||||||
await store.startup();
|
|
||||||
await cryptoStore.startup();
|
|
||||||
await $matrixClient.startClient();
|
|
||||||
$matrixClient.once("sync", function (state, prevState, res) {
|
|
||||||
ready = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStoredLogin(): LoginResponse | undefined {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem("matrix-login")!);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if ($matrixClient) {
|
|
||||||
$matrixClient.stopClient();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $matrixClient && loggedIn}
|
{#if $isLoggedIn}
|
||||||
{#if ready}
|
<div class="layout">
|
||||||
<div class="chat">
|
<nav class="spaces">
|
||||||
<div class="rooms">
|
<a href="/chat/chats" class="icon chats">chat</a>
|
||||||
<button
|
<hr />
|
||||||
onclick={() => {
|
{#if $spaces}
|
||||||
$matrixClient.logout(true);
|
<ul>
|
||||||
$matrixClient.clearStores();
|
{#each $spaces as space (space.roomId)}
|
||||||
localStorage.removeItem("matrix-login");
|
<li animate:flip transition:slide>
|
||||||
window.location.reload();
|
<a class="space" href="/chat/space/{space.roomId}">
|
||||||
}}>logout</button
|
{spaceShort(space.name)}
|
||||||
>
|
</a>
|
||||||
<MatrixRooms rooms={$matrixClient.getRooms()} />
|
</li>
|
||||||
</div>
|
|
||||||
{#if $currentRoomId}
|
|
||||||
{@const room = $matrixClient.getRoom($currentRoomId)}
|
|
||||||
{#key room}
|
|
||||||
{#if room}
|
|
||||||
<div class="timeline">
|
|
||||||
<MatrixTimeline timeline={room.getLiveTimeline()} />
|
|
||||||
</div>
|
|
||||||
<div class="members">
|
|
||||||
<MatrixRoomMembers members={room.getJoinedMembers()} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{:else if $matrixClient}
|
|
||||||
{#await $matrixClient.loginFlows() then flows}
|
|
||||||
{#each flows.flows as flow}
|
|
||||||
{#if flow.type === "m.login.sso"}
|
|
||||||
<a
|
|
||||||
href={$matrixClient.getSsoLoginUrl(`${window.location.origin}/chat/`)}
|
|
||||||
>
|
|
||||||
{#each flow.identity_providers as idp}
|
|
||||||
{#if idp.icon}
|
|
||||||
<img src={$matrixClient.mxcUrlToHttp(idp.icon)} alt={idp.name} />
|
|
||||||
{:else}
|
|
||||||
{idp.name}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
</a>
|
</ul>
|
||||||
{:else if flow.type === "m.login.password"}
|
|
||||||
<form onsubmit={passwordLogin}>
|
|
||||||
<input name="username" type="text" placeholder="Username" />
|
|
||||||
<input name="password" type="password" placeholder="Password" />
|
|
||||||
<button type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
<button class="icon">add</button>
|
||||||
{/await}
|
</nav>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Login />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.chat {
|
nav {
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
> *:not(:last-child) {
|
|
||||||
border-right: 1px solid var(--md-sys-color-outline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.room {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline {
|
.layout {
|
||||||
flex-grow: 1;
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rooms {
|
hr {
|
||||||
flex-shrink: 0;
|
width: 60%;
|
||||||
|
height: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.members {
|
ul {
|
||||||
width: 200px;
|
list-style: none;
|
||||||
flex-shrink: 0;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chats {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
33
src/routes/(app)/chat/Login.svelte
Normal file
33
src/routes/(app)/chat/Login.svelte
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { matrixClient } from "$lib/chat/chat";
|
||||||
|
|
||||||
|
function passwordLogin() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $matrixClient}
|
||||||
|
{#await $matrixClient.loginFlows() then flows}
|
||||||
|
{#each flows.flows as flow}
|
||||||
|
{#if flow.type === "m.login.sso"}
|
||||||
|
<a
|
||||||
|
href={$matrixClient.getSsoLoginUrl(`${window.location.origin}/chat/`)}
|
||||||
|
>
|
||||||
|
{#each flow.identity_providers as idp}
|
||||||
|
{#if idp.icon}
|
||||||
|
<img src={$matrixClient.mxcUrlToHttp(idp.icon)} alt={idp.name} />
|
||||||
|
{:else}
|
||||||
|
{idp.name}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</a>
|
||||||
|
{:else if flow.type === "m.login.password"}
|
||||||
|
<form onsubmit={passwordLogin}>
|
||||||
|
<input name="username" type="text" placeholder="Username" />
|
||||||
|
<input name="password" type="password" placeholder="Password" />
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
0
src/routes/(app)/chat/Space.svelte
Normal file
0
src/routes/(app)/chat/Space.svelte
Normal file
Reference in New Issue
Block a user