feat: matrix

This commit is contained in:
2024-09-13 21:35:52 +02:00
parent 26c43b1966
commit d1fefb88a1
24 changed files with 1386 additions and 59 deletions

View File

@@ -56,3 +56,9 @@
{@render children()}
</main>
{/if}
<style lang="scss">
main {
padding: 0;
}
</style>

View File

@@ -1 +1,184 @@
<h2>WIP</h2>
<script lang="ts">
import { browser } from "$app/environment";
import { onDestroy, onMount, setContext } from "svelte";
import type {
IndexedDBStore,
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 ready = $state(false);
let store: IndexedDBStore;
let cryptoStore: IndexedDBCryptoStore;
onMount(async () => {
if (!browser) return;
const { createClient, IndexedDBStore, IndexedDBCryptoStore } = await import(
"matrix-js-sdk"
);
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>
{#if $matrixClient && loggedIn}
{#if ready}
<div class="chat">
<div class="rooms">
<button
onclick={() => {
$matrixClient.logout(true);
$matrixClient.clearStores();
localStorage.removeItem("matrix-login");
window.location.reload();
}}>logout</button
>
<MatrixRooms rooms={$matrixClient.getRooms()} />
</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}
</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}
<style lang="scss">
.chat {
display: flex;
width: 100%;
height: 100%;
> *:not(:last-child) {
border-right: 1px solid var(--md-sys-color-outline);
}
}
.room {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.timeline {
flex-grow: 1;
}
.rooms {
flex-shrink: 0;
}
.members {
width: 200px;
flex-shrink: 0;
}
</style>

View File

@@ -262,9 +262,7 @@
{/if}
{#each $items.slice(page * $pageSize - (page === 0 ? 0 : 1), (page + 1) * $pageSize - 1) as [chord] (JSON.stringify(chord?.id))}
{#if chord}
<tr>
<ChordEdit {chord} onduplicate={() => (page = 0)} />
</tr>
<ChordEdit {chord} onduplicate={() => (page = 0)} />
{/if}
{/each}</tbody
>
@@ -397,7 +395,7 @@
table {
height: fit-content;
overflow: hidden;
overflow-y: hidden;
transition: all 1s ease;
}
</style>

View File

@@ -89,33 +89,37 @@
}
</script>
<th>
<ChordActionEdit {chord} onsubmit={() => {}} />
</th>
<td>
<ChordPhraseEdit {chord} />
</td>
<td class="table-buttons">
{#if !chord.deleted}
<button transition:slide class="icon compact" onclick={remove}
>delete</button
>
{:else}
<button transition:slide class="icon compact" onclick={restore}
>restore_from_trash</button
>
{/if}
<button disabled={chord.deleted} class="icon compact" onclick={duplicate}
>content_copy</button
>
<button
class="icon compact"
class:disabled={chord.isApplied}
onclick={restore}>undo</button
>
<div class="separator"></div>
<button class="icon compact" onclick={share}>share</button>
</td>
<tr>
<th>
<ChordActionEdit {chord} onsubmit={() => {}} />
</th>
<td class="phrase-edit">
<ChordPhraseEdit {chord} />
</td>
<td>
<div class="table-buttons">
{#if !chord.deleted}
<button transition:slide class="icon compact" onclick={remove}
>delete</button
>
{:else}
<button transition:slide class="icon compact" onclick={restore}
>restore_from_trash</button
>
{/if}
<button disabled={chord.deleted} class="icon compact" onclick={duplicate}
>content_copy</button
>
<button
class="icon compact"
class:disabled={chord.isApplied}
onclick={restore}>undo</button
>
<div class="separator"></div>
<button class="icon compact" onclick={share}>share</button>
</div>
</td>
</tr>
<style lang="scss">
.separator {
@@ -132,17 +136,29 @@
transition: opacity 75ms ease;
}
td {
.phrase-edit {
position: relative;
}
tr {
position: relative;
}
.table-buttons {
opacity: 0;
transition: opacity 75ms ease;
position: absolute;
top: 0;
right: 0;
transform: translate(100%, -50%);
background: var(--md-sys-color-surface-variant);
}
:global(tr):focus-within > .table-buttons,
:global(tr):hover > .table-buttons {
.icon {
font-size: 18px;
}
tr:hover .table-buttons {
opacity: 1;
}
</style>

View File

@@ -0,0 +1,21 @@
<div class="table-buttons">
{#if !chord.deleted}
<button transition:slide class="icon compact" onclick={remove}
>delete</button
>
{:else}
<button transition:slide class="icon compact" onclick={restore}
>restore_from_trash</button
>
{/if}
<button disabled={chord.deleted} class="icon compact" onclick={duplicate}
>content_copy</button
>
<button
class="icon compact"
class:disabled={chord.isApplied}
onclick={restore}>undo</button
>
<div class="separator"></div>
<button class="icon compact" onclick={share}>share</button>
</div>

View File

View File