random stuff
This commit is contained in:
parent
0000149de1
commit
00001504db
34 changed files with 483 additions and 137 deletions
|
@ -29,6 +29,7 @@
|
|||
"@lexical/utils": "^0.5.0",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"discount": "link:../discount",
|
||||
"discount.js": "link:../discount",
|
||||
"emojibase": "^6.1.0",
|
||||
"emojibase-data": "^7.0.1",
|
||||
"fuzzysort": "^2.0.3",
|
||||
|
|
|
@ -13,6 +13,7 @@ specifiers:
|
|||
'@types/better-sqlite3': ^7.6.2
|
||||
'@types/marked': ^4.0.7
|
||||
discount: link:../discount
|
||||
discount.js: link:../discount
|
||||
emojibase: ^6.1.0
|
||||
emojibase-data: ^7.0.1
|
||||
fuzzysort: ^2.0.3
|
||||
|
@ -38,6 +39,7 @@ dependencies:
|
|||
'@lexical/utils': 0.5.0_lexical@0.5.0
|
||||
'@matrix-org/olm': '@gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz'
|
||||
discount: link:../discount
|
||||
discount.js: link:../discount
|
||||
emojibase: 6.1.0
|
||||
emojibase-data: 7.0.1_emojibase@6.1.0
|
||||
fuzzysort: 2.0.3
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Api from "../matrix/api.js";
|
||||
import Settings from "../matrix/settings.js";
|
||||
import PushRules from "../../util/push.js";
|
||||
import { Client, persist } from "discount";
|
||||
import { Client, persist } from "discount.js";
|
||||
|
||||
const defaultFilter = {
|
||||
room: {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { writable, get } from "svelte/store";
|
||||
import type { Room } from "discount";
|
||||
import type { Room } from "discount.js";
|
||||
|
||||
export function handleAccount(_: void, event: { type: string, content: any }) {
|
||||
if (event.type === "m.fully_read") {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type { Event } from "discount";
|
||||
import type { Room } from "discount";
|
||||
import type { Room, Event } from "discount.js";
|
||||
|
||||
export function reslice(room: Room, force = false) {
|
||||
if (!room.events.live) return;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Room } from "discount";
|
||||
import type { Room } from "discount.js";
|
||||
|
||||
interface Notifications {
|
||||
level: "default" | "all" | "mentions" | "muted",
|
||||
|
|
|
@ -17,8 +17,11 @@ defaultSettings.set("sendtyping", true);
|
|||
defaultSettings.set("autocomplete", true);
|
||||
defaultSettings.set("showembeds", "unencrypted");
|
||||
|
||||
// rooms and spaces
|
||||
defaultSettings.set("autospace", false);
|
||||
defaultSettings.set("autodm", false);
|
||||
|
||||
// misc
|
||||
defaultSettings.set("autojoin", true);
|
||||
|
||||
export default class Settings extends Map {
|
||||
constructor(data) {
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import { openDB, deleteDB } from "idb";
|
||||
import { Event } from "discount";
|
||||
|
||||
class PersistentMap extends Map {
|
||||
constructor(name, db) {
|
||||
super();
|
||||
this.name = name;
|
||||
this._db = db;
|
||||
}
|
||||
|
||||
async fetch(key) {
|
||||
return super.get(key) ?? await this._db.get(this.name, key);
|
||||
}
|
||||
|
||||
async all() {
|
||||
return (await this._db.all(this.name)).map(i => i);
|
||||
}
|
||||
|
||||
async put(key, val) {
|
||||
super.set(key, val);
|
||||
await this._db.put(this.name, val, key);
|
||||
}
|
||||
}
|
||||
|
||||
class Events extends PersistentMap {
|
||||
async fetch(room, eventId) {
|
||||
if (this.has(eventId)) return this.get(eventId);
|
||||
// const fromDb = await this._db.get(this.name, eventId);
|
||||
const event = new Event(room, (await state.api.fetchEvent(room.id, eventId)));
|
||||
// const event = new Event(room, (await this._db.get(this.name, eventId) ?? await state.api.fetchEvent(room.roomId, eventId)));
|
||||
this.set(eventId, event);
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
export default class Store {
|
||||
async init() {
|
||||
const db = await openDB("discard", 1, {
|
||||
upgrade(db) {
|
||||
db.createObjectStore("account");
|
||||
db.createObjectStore("events");
|
||||
db.createObjectStore("rooms");
|
||||
db.createObjectStore("users");
|
||||
db.createObjectStore("sync");
|
||||
},
|
||||
});
|
||||
|
||||
this._db = db;
|
||||
this.events = new Events("events", db);
|
||||
this.account = new PersistentMap("account", db);
|
||||
this.rooms = new PersistentMap("rooms", db);
|
||||
this.sync = new PersistentMap("sync", db);
|
||||
this.users = new PersistentMap("users", db);
|
||||
}
|
||||
|
||||
async purge() {
|
||||
this._db.close();
|
||||
await deleteDB("discard");
|
||||
}
|
||||
|
||||
save(state) {
|
||||
for (let [key, val] of state.accountDataRef) this.account.put(key, val);
|
||||
for (let [id, room] of state.rooms) this.rooms.put(id, {
|
||||
state: room.state,
|
||||
roomId: room.roomId,
|
||||
accountData: [...room.accountData.entries()],
|
||||
notifications: room.notifications,
|
||||
});
|
||||
// for (let [id, event] of state.events) this.events.put(id, {
|
||||
// roomId: event.roomId,
|
||||
// raw: event.raw,
|
||||
// relations: event.relations,
|
||||
// });
|
||||
state.log.matrix("persisting state");
|
||||
}
|
||||
|
||||
load(state) {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { handle } from "../actions/timeline";
|
||||
import { Event, StateEvent } from "discount";
|
||||
import { Event, StateEvent } from "discount.js";
|
||||
|
||||
class Timeline extends Array {
|
||||
constructor(roomId, start, end) {
|
||||
|
|
|
@ -85,7 +85,7 @@ summary {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
button, input[type=text], input[type=password] {
|
||||
button, input[type=text], input[type=password], textarea {
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
.hljs-regexp { color: #95e6cb }
|
||||
.hljs-regex-delimiter { color: #95d8e5 }
|
||||
|
||||
|
||||
.hljs-type { color: #ffb454 }
|
||||
.hljs-type { color: #f29668 }
|
||||
.hljs-type { color: #ff8f40 }
|
||||
|
|
|
@ -74,7 +74,7 @@ function select(option) {
|
|||
transform: scale(1.2);
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<div class="container" role="listbox">
|
||||
<!-- FIXME: close on blur -->
|
||||
<div class="dropdown" class:show on:click={() => show = !show} bind:clientWidth={width}>
|
||||
{options.find(i => i[1] === selected)?.[0]}
|
||||
|
@ -83,12 +83,18 @@ function select(option) {
|
|||
{#if show}
|
||||
<div class="options" style:width={width + "px"}>
|
||||
{#each options as option}
|
||||
<div class="option" class:selected={selected === option[1]} on:click={() => select(option[1])}>
|
||||
<div
|
||||
class="option"
|
||||
role="option"
|
||||
aria-selected={selected === option[1] }
|
||||
class:selected={selected === option[1]}
|
||||
on:click={() => select(option[1])}
|
||||
>
|
||||
<div class="label">
|
||||
{option[0]}
|
||||
</div>
|
||||
{#if option[1] === selected}
|
||||
<div class="checkmark icon">check_circle</div>
|
||||
<div class="checkmark icon" aria-hidden="true">check_circle</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
@ -94,8 +94,20 @@ function opacity() {
|
|||
transition: color 0.4s;
|
||||
}
|
||||
</style>
|
||||
<div class="background" on:mousedown|stopPropagation={closePopup} transition:opacity on:click|stopPropagation>
|
||||
<div class="card" on:mousedown|stopPropagation class:raw transition:card role="dialog" aria-modal="true">
|
||||
<div
|
||||
class="background"
|
||||
on:mousedown|stopPropagation={closePopup}
|
||||
on:click|stopPropagation
|
||||
transition:opacity
|
||||
>
|
||||
<div
|
||||
class="card"
|
||||
class:raw
|
||||
on:mousedown|stopPropagation
|
||||
transition:card
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
{#if showClose}
|
||||
<button class="icon close" on:click={closePopup}>close</button>
|
||||
{/if}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
export let checked = false;
|
||||
export let toggled = () => {};
|
||||
export let toggled = (_checked: boolean) => {};
|
||||
</script>
|
||||
<style>
|
||||
.toggle {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script type="ts">
|
||||
import type { Room } from "discount";
|
||||
import type { Room } from "discount.js";
|
||||
export let room: Room | null = null;
|
||||
export let users: Array<string>;
|
||||
$: [single, plural, typing] = getType(room?.name);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import Avatar from "../atoms/Avatar.svelte";
|
||||
import { memberContext, roomContext } from "../../util/context";
|
||||
import { fastclick } from "../../util/use";
|
||||
import { Room, Member } from "discount";
|
||||
import { Room, Member } from "discount.js";
|
||||
export let member;
|
||||
let { popup, popout, context } = state;
|
||||
$: isRoomPing = member instanceof Room;
|
||||
|
|
|
@ -30,7 +30,6 @@ export let size = 0;
|
|||
padding: 0 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import Button from "../atoms/Button.svelte";
|
|||
import Popup from "../atoms/Popup.svelte";
|
||||
import Search from "../atoms/Search.svelte";
|
||||
import Checkbox from "../atoms/Checkbox.svelte";
|
||||
import type { Room } from "discount";
|
||||
import type { Room } from "discount.js";
|
||||
// export const confirm = () => {};
|
||||
export let current;
|
||||
let checked = {};
|
||||
|
|
|
@ -8,13 +8,16 @@ export const confirm = kick;
|
|||
export let current;
|
||||
let reason;
|
||||
|
||||
const rnd = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
const placeholders = [
|
||||
"they disagree with my opinion",
|
||||
"i dont like them",
|
||||
"they were acting sussy",
|
||||
];
|
||||
|
||||
function getPlaceholder() {
|
||||
return placeholders[Math.floor(Math.random() * placeholders.length)];
|
||||
}
|
||||
|
||||
const scopes = [];
|
||||
if (current.room.type === "m.space") {
|
||||
scopes.push(["This space", "space"]);
|
||||
|
@ -43,7 +46,7 @@ function kick() {
|
|||
<Dropdown options={scopes} />
|
||||
{/if}
|
||||
<div class="title">Reason for Kick</div>
|
||||
<Textarea autofocus placeholder={rnd(placeholders)} bind:value={reason} />
|
||||
<Textarea autofocus placeholder={getPlaceholder()} bind:value={reason} />
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<Button type="link" label="Nevermind!" clicked={() => state.popup.set({ ...current, id: null })} />
|
||||
|
|
|
@ -3,7 +3,7 @@ import fuzzysort from "fuzzysort";
|
|||
import Search from "../atoms/Search.svelte";
|
||||
import Popup from "../atoms/Popup.svelte";
|
||||
import { getLastMessage } from "../../util/timeline";
|
||||
import type { Room } from "discount";
|
||||
import type { Room } from "discount.js";
|
||||
let search = "";
|
||||
let highlighted = 0;
|
||||
$: results = getRooms(search);
|
||||
|
|
|
@ -13,6 +13,7 @@ import UnknownRoom from './rooms/Unknown.svelte';
|
|||
// hmmm...
|
||||
import MediaRoom from './rooms/Media.svelte';
|
||||
import ForumRoom from './rooms/Forum.svelte';
|
||||
import LongformRoom from './rooms/Longform.svelte';
|
||||
|
||||
let { focusedRoom: room, focusedSpace: space, navRooms, roomState, slice, settings } = state;
|
||||
let { search } = roomState;
|
||||
|
@ -56,6 +57,8 @@ $: if ($navRooms) room = state.focusedRoom;
|
|||
<MediaRoom room={$room} slice={$slice} />
|
||||
{:else if type === "org.eu.celery.room.forum"}
|
||||
<ForumRoom room={$room} />
|
||||
{:else if type === "org.eu.celery.room.longform"}
|
||||
<LongformRoom room={$room} slice={$slice} />
|
||||
{:else}
|
||||
<UnknownRoom room={$room} />
|
||||
{/if}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import Typing from "../atoms/Typing.svelte";
|
||||
import RoomInput from "./RoomInput.svelte";
|
||||
let textarea;
|
||||
let textareaEl;
|
||||
let room = state.focusedRoom;
|
||||
let { reply, edit, input, typing } = state.roomState;
|
||||
|
||||
|
@ -27,10 +27,10 @@ function getRoomName(room) {
|
|||
return other?.name ?? other?.id;
|
||||
}
|
||||
|
||||
$: if ($input) textarea?.focus();
|
||||
$: if ($room) textarea?.focus();
|
||||
$: if ($reply || true) textarea?.focus();
|
||||
$: if (!$edit) textarea?.focus();
|
||||
$: if ($input) textareaEl?.focus();
|
||||
$: if ($room) textareaEl?.focus();
|
||||
$: if ($reply || true) textareaEl?.focus();
|
||||
$: if (!$edit) textareaEl?.focus();
|
||||
</script>
|
||||
<style>
|
||||
.container {
|
||||
|
@ -77,12 +77,12 @@ $: if (!$edit) textarea?.focus();
|
|||
<div class="input disabled"><div class="center">You can't send messages in a e2ee room yet</div></div>
|
||||
{:else}
|
||||
<RoomInput
|
||||
showUpload={true}
|
||||
showUpload
|
||||
placeholder={`Message ${getRoomName($room)}`}
|
||||
onsend={sendMessage}
|
||||
bind:input={$input}
|
||||
bind:reply={$reply}
|
||||
bind:textarea={textarea}
|
||||
bind:textarea={textareaEl}
|
||||
/>
|
||||
{/if}
|
||||
<div class="typing">
|
||||
|
|
|
@ -15,6 +15,8 @@ export let input = "";
|
|||
export let textarea;
|
||||
let showEmoji = false;
|
||||
|
||||
export let asdfasdfasdf = false;
|
||||
|
||||
let { focusedRoom: room, focusedSpace, slice, popup } = state;
|
||||
let { edit } = state.roomState;
|
||||
|
||||
|
@ -144,6 +146,7 @@ async function handleUpload(file) {
|
|||
mimetype: file.type,
|
||||
size: file.size,
|
||||
...(["m.image", "m.video"].includes(type) ? await getSize(file, type) : {}),
|
||||
filename: file.name,
|
||||
};
|
||||
|
||||
onsend({
|
||||
|
@ -237,11 +240,16 @@ function slide() {
|
|||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.input.asdfasdfasdf {
|
||||
border-radius: 4px;
|
||||
background: var(--bg-misc);
|
||||
}
|
||||
|
||||
.upload {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
align-items: start;
|
||||
padding: 8px 16px 0;
|
||||
font-size: 24px;
|
||||
color: var(--fg-light);
|
||||
cursor: pointer;
|
||||
|
@ -310,14 +318,14 @@ function slide() {
|
|||
onclose={() => reply = null}
|
||||
/>
|
||||
{/if}
|
||||
<div class="input" class:withreply={reply}>
|
||||
<div class="input" class:asdfasdfasdf class:withreply={reply}>
|
||||
{#if showUpload}
|
||||
<label class="upload">
|
||||
<div class="icon">add_circle</div>
|
||||
<input type="file" on:change={e => onfile(e.target.files[0])} />
|
||||
</label>
|
||||
{/if}
|
||||
<RoomTextarea {placeholder} bind:input={input} bind:textarea={textarea} {onfile} {oninput} />
|
||||
<RoomTextarea {placeholder} {asdfasdfasdf} bind:input={input} bind:textarea={textarea} {onfile} {oninput} />
|
||||
<!--
|
||||
<div class="icon" style="font-size: 28px; color: var(--fg-light)">gif_box</div>
|
||||
<div class="icon" style="font-size: 28px; margin-left: 8px; color: var(--fg-light)">sticky_note_2</div>
|
||||
|
|
|
@ -5,7 +5,8 @@ export let onfile = () => {};
|
|||
export let textarea;
|
||||
export let placeholder;
|
||||
export let input = "";
|
||||
$: rows = Math.min(input.split("\n").length, 10);
|
||||
export let asdfasdfasdf = false; // i am very good at naming
|
||||
$: rows = asdfasdfasdf ? 8 : Math.min(input.split("\n").length, 10);
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.ctrlKey) {
|
||||
|
@ -40,7 +41,7 @@ function handleKeyDown(e) {
|
|||
// case "u": return wrapInsert("__");
|
||||
}
|
||||
}
|
||||
if (e.key !== "Enter" || e.shiftKey) return;
|
||||
if (e.key !== "Enter" || (asdfasdfasdf ? !e.ctrlKey : e.shiftKey)) return;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
|
@ -57,13 +58,7 @@ function handlePaste(e) {
|
|||
</script>
|
||||
<style>
|
||||
textarea {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
@ -71,10 +66,14 @@ textarea {
|
|||
textarea::placeholder {
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
textarea.asdfasdfasdf {
|
||||
}
|
||||
</style>
|
||||
<textarea
|
||||
{placeholder}
|
||||
{rows}
|
||||
class:asdfasdfasdf
|
||||
bind:this={textarea}
|
||||
bind:value={input}
|
||||
on:keydown={handleKeyDown}
|
||||
|
|
|
@ -44,13 +44,12 @@ const showMemberContext = (member) => (e) => {
|
|||
display: flex;
|
||||
padding: 2px 72px 4px;
|
||||
padding-left: 0;
|
||||
position: relative;
|
||||
color: var(--fg-content);
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--fg-content);
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.author {
|
||||
|
|
|
@ -49,7 +49,7 @@ let eventPromise = room.events.fetch(eventId);
|
|||
}
|
||||
|
||||
.content {
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -70,7 +70,7 @@ function checkShift(e) {
|
|||
}
|
||||
|
||||
function getHighlight(event, reply) {
|
||||
if (reply?.eventId === event.id) return "var(--color-blue)";
|
||||
if (reply?.id === event.id) return "var(--color-blue)";
|
||||
}
|
||||
|
||||
function shouldRender(_type, _settings) {
|
||||
|
@ -81,12 +81,19 @@ function shouldRender(_type, _settings) {
|
|||
}
|
||||
|
||||
function shouldPing(event) {
|
||||
// FIXME: doesnt parse pings in muted rooms
|
||||
const parsed = $pushRules.parse(event);
|
||||
if (!parsed) return false;
|
||||
const highlight = parsed.actions.find(i => i.set_tweak === "highlight");
|
||||
if (!highlight) return false;
|
||||
return highlight.value !== false;
|
||||
let highlight = false;
|
||||
parsed.reverse();
|
||||
for (let { actions } of parsed) {
|
||||
const hl = actions.find(a => a.set_tweak === "highlight");
|
||||
if (!hl) continue;
|
||||
if (hl.value === false) {
|
||||
highlight = false;
|
||||
} else {
|
||||
highlight = true;
|
||||
}
|
||||
}
|
||||
return highlight;
|
||||
}
|
||||
|
||||
onDestroy(state.focusedRoom.subscribe(() => {
|
||||
|
|
364
src/ui/room/rooms/Longform.svelte
Normal file
364
src/ui/room/rooms/Longform.svelte
Normal file
|
@ -0,0 +1,364 @@
|
|||
<script>
|
||||
import { onDestroy } from "svelte";
|
||||
import Scroller from '../../molecules/Scroller.svelte';
|
||||
import Upload from '../timeline/Upload.svelte';
|
||||
import Placeholder from '../timeline/Placeholder.svelte';
|
||||
import Create from "../events/Create.svelte";
|
||||
import MessageReply from "../message/MessageReply.svelte";
|
||||
import MessageContent from "../message/MessageContent.svelte";
|
||||
import MessageEdit from "../message/MessageEdit.svelte";
|
||||
import RoomInput from "../RoomInput.svelte";
|
||||
import { formatDate } from "../../../util/format.ts";
|
||||
import Avatar from "../../atoms/Avatar.svelte";
|
||||
import Name from "../../atoms/Name.svelte";
|
||||
import { memberContext } from "../../../util/context";
|
||||
export let room;
|
||||
export let slice;
|
||||
let { focused, reply, edit, upload, input } = state.roomState;
|
||||
let { pushRules, popout, context } = state;
|
||||
let scrollTop, scrollMax, scrollTo, reset;
|
||||
let textareaEl;
|
||||
|
||||
$: if (slice) refocus();
|
||||
|
||||
function getReply(content) {
|
||||
return content["m.relates_to"]?.["m.in_reply_to"]?.event_id;
|
||||
}
|
||||
|
||||
async function fetchBackwards() {
|
||||
const success = await actions.slice.backwards();
|
||||
return [!success || slice.events[0]?.type === "m.room.create", slice.atEnd()];
|
||||
}
|
||||
|
||||
async function fetchForwards() {
|
||||
const success = await actions.slice.forwards();
|
||||
return [!success || slice.events[0]?.type === "m.room.create", slice.atEnd()];
|
||||
}
|
||||
|
||||
function refocus() {
|
||||
if (scrollTo && scrollTop > scrollMax - 16) {
|
||||
queueMicrotask(() => scrollTo(-1));
|
||||
}
|
||||
}
|
||||
|
||||
let resizeTimeout;
|
||||
function handleResize() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(refocus, 50);
|
||||
}
|
||||
|
||||
function getHighlight(event, reply) {
|
||||
if (reply?.id === event.id) return "var(--color-blue)";
|
||||
}
|
||||
|
||||
function shouldPing(event) {
|
||||
const parsed = $pushRules.parse(event);
|
||||
let highlight = false;
|
||||
parsed.reverse();
|
||||
for (let { actions } of parsed) {
|
||||
const hl = actions.find(a => a.set_tweak === "highlight");
|
||||
if (!hl) continue;
|
||||
if (hl.value === false) {
|
||||
highlight = false;
|
||||
} else {
|
||||
highlight = true;
|
||||
}
|
||||
}
|
||||
return highlight;
|
||||
}
|
||||
|
||||
const showMemberPopout = (event) => (e) => {
|
||||
const rect = e.target.getBoundingClientRect();
|
||||
const _owner = `${event.id}-${event.sender.id}`;
|
||||
if ($popout._owner === _owner) return $popout = {};
|
||||
$popout = {
|
||||
id: "member",
|
||||
member: event.sender,
|
||||
animate: "right",
|
||||
top: rect.y,
|
||||
left: rect.x + rect.width + 8,
|
||||
_owner,
|
||||
};
|
||||
}
|
||||
|
||||
const showMemberContext = (member) => (e) => {
|
||||
$context = {
|
||||
items: memberContext(member),
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
}
|
||||
|
||||
onDestroy(state.focusedRoom.subscribe(() => {
|
||||
console.time("focus room")
|
||||
queueMicrotask(() => console.timeEnd("focus room"));
|
||||
}));
|
||||
|
||||
onDestroy(state.focusedRoom.subscribe(() => queueMicrotask(() => reset && reset())));
|
||||
onDestroy(upload.subscribe(refocus));
|
||||
onDestroy(reply.subscribe(refocus));
|
||||
onDestroy(edit.subscribe(refocus));
|
||||
$: if ($focused) {
|
||||
const id = $focused;
|
||||
const element = document.querySelector(`[data-event-id="${id}"]`);
|
||||
if (element) {
|
||||
queueMicrotask(() => element.scrollIntoView({ behavior: "smooth", block: "center" }));
|
||||
setTimeout(() => id === $focused && focused.set(null), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($edit) {
|
||||
const element = document.querySelector(`[data-event-id="${$edit}"]`);
|
||||
if (element) {
|
||||
setTimeout(() => element.scrollIntoView({ behavior: "smooth", block: "center" }));
|
||||
}
|
||||
}
|
||||
|
||||
$: if (room) textareaEl?.focus();
|
||||
$: if ($reply || true) textareaEl?.focus();
|
||||
$: if (!$edit) textareaEl?.focus();
|
||||
|
||||
async function sendMessage(content) {
|
||||
if ($reply) {
|
||||
content["m.relates_to"] = {};
|
||||
content["m.relates_to"]["m.in_reply_to"] = {};
|
||||
content["m.relates_to"]["m.in_reply_to"]["event_id"] = $reply.id;
|
||||
reply.set(null);
|
||||
}
|
||||
|
||||
const id = `~${Math.random().toString(36).slice(2)}`;
|
||||
room.accountData.set("m.fully_read", id);
|
||||
room.sendEvent("m.room.message", content, id);
|
||||
state.log.debug("send event to " + room.id);
|
||||
}
|
||||
|
||||
function getRoomName(room) {
|
||||
if (room.name) return room.name;
|
||||
const other = state.dms.get(room.id);
|
||||
return other?.name ?? other?.id;
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.tall {
|
||||
display: flex;
|
||||
min-height: 1200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.highlight::before, .highlight::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: var(--color);
|
||||
}
|
||||
|
||||
.highlight::after {
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
.highlight::before {
|
||||
width: 100%;
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
.ping {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editing {
|
||||
position: relative;
|
||||
background: rgba(4,4,5,0.07);
|
||||
}
|
||||
|
||||
.focused {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ping::before, .ping::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: var(--event-ping);
|
||||
}
|
||||
|
||||
.ping::before {
|
||||
width: 100%;
|
||||
opacity: .1;
|
||||
}
|
||||
|
||||
.ping::after {
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
.focused {
|
||||
background: var(--event-focus-bg);
|
||||
animation: unfocus 1s 1s forwards;
|
||||
}
|
||||
|
||||
@keyframes unfocus {
|
||||
100% { background: none }
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 14px 16px;
|
||||
padding: 8px;
|
||||
background: var(--bg-rooms-members);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.author {
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
box-shadow: 0 0 0 #00000022;
|
||||
cursor: pointer;
|
||||
transition: box-shadow .2s;
|
||||
}
|
||||
|
||||
.avatar:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.avatar:hover, .avatar.selected {
|
||||
box-shadow: 0 4px 4px #00000022;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
bottom: 2px;
|
||||
|
||||
color: var(--fg-notice);
|
||||
background: var(--color-accent);
|
||||
font-family: var(--font-display);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
time {
|
||||
color: var(--fg-muted);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-display);
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
margin-top: 8px;
|
||||
color: var(--fg-content);
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<Scroller
|
||||
items={slice.events}
|
||||
itemKey="id"
|
||||
direction="up"
|
||||
bind:scrollTop={scrollTop}
|
||||
bind:scrollMax={scrollMax}
|
||||
bind:scrollTo={scrollTo}
|
||||
bind:reset={reset}
|
||||
let:data={event}
|
||||
{fetchBackwards}
|
||||
{fetchForwards}
|
||||
getDefault={() => [slice.events[0]?.type === "m.room.create", slice.atEnd()]}
|
||||
>
|
||||
<div slot="top" style="margin-top: auto"></div>
|
||||
<div slot="placeholder-start" class="tall" style="align-items: end"><Placeholder /></div>
|
||||
<div>
|
||||
{#if event.type === "m.room.message"}
|
||||
<div
|
||||
class="message"
|
||||
class:ping={shouldPing(event)}
|
||||
class:focused={$focused === event.id}
|
||||
class:editing={$edit === event.id}
|
||||
data-event-id={event.id}
|
||||
class:highlight={getHighlight(event, $reply)}
|
||||
style:--color={getHighlight(event, $reply)}
|
||||
on:contextmenu={e => { const memberId = e.target.dataset.mxPing; if (memberId && room.members.has(memberId)) { e.preventDefault(); e.stopPropagation(); showMemberContext(room.members.get(memberId)) }}}
|
||||
>
|
||||
<div class="header">
|
||||
{#if getReply(event.content)}<MessageReply {room} eventId={getReply(event.content)} />{/if}
|
||||
<div
|
||||
class="avatar"
|
||||
class:selected={$popout._owner === `${event.id}-${event.sender.id}`}
|
||||
on:click|stopPropagation={showMemberPopout(event)}
|
||||
on:contextmenu|preventDefault|stopPropagation={showMemberContext(event.sender)}
|
||||
>
|
||||
<Avatar user={event.sender} size={40} />
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; margin-left: 8px;">
|
||||
<div class="author">
|
||||
<Name member={event.sender} />
|
||||
{#if event.content.msgtype === "m.notice"}
|
||||
<div class="badge">bot</div>
|
||||
{/if}
|
||||
</div>
|
||||
<time datetime={event.date.toISOString()} style="display: inline">{formatDate(event.date)}</time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
{#if event.id === $edit}
|
||||
<MessageEdit {event} />
|
||||
{:else}
|
||||
<MessageContent {event} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if event.type === "m.room.create"}
|
||||
<Create {room} {event} />
|
||||
{/if}
|
||||
</div>
|
||||
<div slot="placeholder-end" class="tall"><Placeholder /></div>
|
||||
<div slot="bottom" class="spacer">
|
||||
{#if $upload}<Upload upload={$upload} />{/if}
|
||||
<div style="padding: 0 16px; padding-bottom: 14px;">
|
||||
<RoomInput
|
||||
asdfasdfasdf
|
||||
showUpload
|
||||
placeholder={`Message ${getRoomName(room)}`}
|
||||
onsend={sendMessage}
|
||||
bind:input={$input}
|
||||
bind:reply={$reply}
|
||||
bind:textarea={textareaEl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
<svelte:window on:resize={handleResize} />
|
|
@ -14,5 +14,9 @@ let settings = state.settings;
|
|||
</style>
|
||||
<div class="setting">
|
||||
<div>Automatically join suggested rooms</div>
|
||||
<Toggle checked={$settings.get("autojoin")} toggled={(val) => $settings.put("autojoin", val)} />
|
||||
<Toggle checked={$settings.get("autospace")} toggled={(val) => $settings.put("autospace", val)} />
|
||||
</div>
|
||||
<div class="setting">
|
||||
<div>Automatically accept dms</div>
|
||||
<Toggle checked={$settings.get("autodm")} toggled={(val) => $settings.put("autodm", val)} />
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Event, Room, Member } from "discount";
|
||||
import type { Event, Room, Member } from "discount.js";
|
||||
import { getRoomNotifRule, putRoomNotifRule } from "../client/matrix/notifications";
|
||||
import { get } from "svelte/store";
|
||||
import * as notif from "../client/matrix/notifications";
|
||||
|
|
|
@ -102,6 +102,14 @@ function matchesRule(cond, event) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// function matchBuiltinRule(ruleId, event) {
|
||||
// switch (ruleId) {
|
||||
// case ".m.rule.master": return true;
|
||||
// case ".m.rule.suppress_notices": return event.;
|
||||
// default: return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
export default class PushRules {
|
||||
constructor(rules) {
|
||||
this.setRules(rules);
|
||||
|
@ -119,7 +127,8 @@ export default class PushRules {
|
|||
}
|
||||
|
||||
parse(event) {
|
||||
if (event.sender.userId === state.userId) return null;
|
||||
if (event.sender.userId === state.userId) return [];
|
||||
const rules = [];
|
||||
for (let rule of this.rules) {
|
||||
let passed = true;
|
||||
for (let cond of rule.conditions) {
|
||||
|
@ -128,8 +137,8 @@ export default class PushRules {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (passed) return rule;
|
||||
if (passed) rules.push(rule);
|
||||
}
|
||||
return null;
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// import type { Timeline } from "discount";
|
||||
// import type { Event } from "discount";
|
||||
// import type { Timeline } from "discount.js";
|
||||
// import type { Event } from "discount.js";
|
||||
|
||||
// export function getLastEvent(timeline: Array<Event>): Event | null {
|
||||
// for (let i = timeline.length - 1; i >= 0; i--) {
|
||||
|
|
|
@ -2,5 +2,7 @@
|
|||
"extends": "@tsconfig/svelte",
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/*"],
|
||||
"target": "es2022",
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,4 +35,9 @@ export default defineConfig({
|
|||
commit: execSync("git log -n 1 --oneline HEAD").toString().match(/[a-z0-9]+/)[0],
|
||||
}
|
||||
},
|
||||
server: {
|
||||
hmr: {
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Reference in a new issue