some more stuff
This commit is contained in:
parent
0000050037
commit
000005102e
33 changed files with 363 additions and 60 deletions
|
@ -17,6 +17,8 @@
|
|||
"vite": "^2.9.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"emojibase": "^6.1.0",
|
||||
"emojibase-data": "^7.0.1",
|
||||
"idb": "^7.0.2",
|
||||
"linkifyjs": "^3.0.5",
|
||||
"marked": "^4.0.17",
|
||||
|
|
|
@ -3,6 +3,8 @@ lockfileVersion: 5.4
|
|||
specifiers:
|
||||
'@sveltejs/vite-plugin-svelte': next
|
||||
'@types/marked': ^4.0.3
|
||||
emojibase: ^6.1.0
|
||||
emojibase-data: ^7.0.1
|
||||
idb: ^7.0.2
|
||||
linkifyjs: ^3.0.5
|
||||
marked: ^4.0.17
|
||||
|
@ -11,6 +13,8 @@ specifiers:
|
|||
vite: ^2.9.14
|
||||
|
||||
dependencies:
|
||||
emojibase: 6.1.0
|
||||
emojibase-data: 7.0.1_emojibase@6.1.0
|
||||
idb: 7.0.2
|
||||
linkifyjs: 3.0.5
|
||||
marked: 4.0.17
|
||||
|
@ -103,6 +107,18 @@ packages:
|
|||
domhandler: 4.3.1
|
||||
dev: false
|
||||
|
||||
/emojibase-data/7.0.1_emojibase@6.1.0:
|
||||
resolution: {integrity: sha512-BLZpOdwyFpZ7lzBWyDtnxmKVm/SJMYgAfp1if3o6n1TVUMSXAf0nikONXl90LZuJ/m3XWPBkkubgCet2BsCGGQ==}
|
||||
peerDependencies:
|
||||
emojibase: '*'
|
||||
dependencies:
|
||||
emojibase: 6.1.0
|
||||
dev: false
|
||||
|
||||
/emojibase/6.1.0:
|
||||
resolution: {integrity: sha512-1GkKJPXP6tVkYJHOBSJHoGOr/6uaDxZ9xJ6H7m6PfdGXTmQgbALHLWaVRY4Gi/qf5x/gT/NUXLPuSHYLqtLtrQ==}
|
||||
dev: false
|
||||
|
||||
/entities/2.2.0:
|
||||
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
||||
dev: false
|
||||
|
|
19
src/.pnpm-debug.log
Normal file
19
src/.pnpm-debug.log
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"0 debug pnpm:scope": {
|
||||
"selected": 1
|
||||
},
|
||||
"1 error pnpm": {
|
||||
"errno": 1,
|
||||
"code": "ELIFECYCLE",
|
||||
"pkgid": "discard@0.1.0",
|
||||
"stage": "build",
|
||||
"script": "vite build",
|
||||
"pkgname": "discard",
|
||||
"err": {
|
||||
"name": "pnpm",
|
||||
"message": "discard@0.1.0 build: `vite build`\nExit status 1",
|
||||
"code": "ELIFECYCLE",
|
||||
"stack": "pnpm: discard@0.1.0 build: `vite build`\nExit status 1\n at EventEmitter.<anonymous> (/usr/lib/node_modules/pnpm/dist/pnpm.cjs:108597:20)\n at EventEmitter.emit (node:events:537:28)\n at ChildProcess.<anonymous> (/usr/lib/node_modules/pnpm/dist/pnpm.cjs:95163:18)\n at ChildProcess.emit (node:events:537:28)\n at maybeClose (node:internal/child_process:1091:16)\n at ChildProcess._handle.onexit (node:internal/child_process:302:5)"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ function queueRelation(id, event) {
|
|||
relations.get(id).push(event);
|
||||
}
|
||||
|
||||
export function getTimeline(roomId) {
|
||||
function getTimeline(roomId) {
|
||||
return state.roomTimelines.get(roomId).live;
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,6 @@ export function send(roomId, type, content) {
|
|||
}
|
||||
|
||||
export function handle(roomId, event, toStart = false) {
|
||||
console.log(roomId, event)
|
||||
if (event.type === "m.room.redaction" && !toStart) return redact(roomId, event);
|
||||
if (!supportedEvents.includes(event.type)) return;
|
||||
if (event.unsigned?.redacted_because) return;
|
||||
|
@ -110,17 +109,23 @@ export function handle(roomId, event, toStart = false) {
|
|||
eventId: original.eventId, // this is why the todo exists
|
||||
original,
|
||||
});
|
||||
console.log("handle edit")
|
||||
} else if (relation.rel_type === "m.annotation") {
|
||||
const key = relation.key;
|
||||
const key = relation.key;
|
||||
if (!original.reactions) original.reactions = new Map();
|
||||
if (!original.reactions.has(key)) original.reactions.set(key, [0, null]);
|
||||
if (event.sender === state.userId) original.reactions.get(relation.key)[1] = id;
|
||||
original.reactions.get(relation.key)[0]++;
|
||||
}
|
||||
state.events.set(id, format(roomId, event));
|
||||
const slice = actions.slice.get(roomId);
|
||||
slice.reslice();
|
||||
if (roomId === state.focusedRoomId) state.slice.set(slice);
|
||||
} else if(event.state_key) {
|
||||
actions.rooms.update();
|
||||
actions.spaces.update();
|
||||
if (roomId === state.focusedRoomId) state.slice.set(actions.slice.get(roomId));
|
||||
} else {
|
||||
// TODO: update on state events
|
||||
state.events.set(id, format(roomId, event));
|
||||
addToTimeline(roomId, id, toStart);
|
||||
if (relations.has(id)) {
|
||||
|
|
|
@ -138,7 +138,7 @@ export default class Api {
|
|||
...(type && {
|
||||
creation_content: { type },
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
joinRoom() { throw "TODO" }
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
const defaultSettings = new Map();
|
||||
defaultSettings.set("namecolors", "always");
|
||||
defaultSettings.set("showjoinleave", true);
|
||||
defaultSettings.set("showmembership", true);
|
||||
defaultSettings.set("shownickavatar", true);
|
||||
defaultSettings.set("shownametopic", true);
|
||||
defaultSettings.set("autojoin", true);
|
||||
|
||||
export default class Settings extends Map {
|
||||
constructor(data) {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<base target="_blank" />
|
||||
<title>discard</title>
|
||||
<link href="/style.css" rel="stylesheet"/>
|
||||
<link href="/fonts.css" rel="stylesheet"/>
|
||||
<link href="/elements.css" rel="stylesheet"/>
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
import UserId from "./atoms/UserId.svelte";
|
||||
import Button from "./atoms/Button.svelte";
|
||||
import Input from "./atoms/Input.svelte";
|
||||
import Textarea from "./atoms/Textarea.svelte";
|
||||
import Dropdown from "./atoms/Dropdown.svelte";
|
||||
import Popup from "./atoms/Popup.svelte";
|
||||
import Logout from "./popups/Logout.svelte";
|
||||
import Leave from "./popups/Leave.svelte";
|
||||
|
@ -92,7 +94,7 @@ current.subscribe(reset);
|
|||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 16px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
@ -150,13 +152,13 @@ pre {
|
|||
<div slot="content" style:margin-top="-8px">
|
||||
{#if ["invite", "ban"].includes($current.membership)}
|
||||
<div style:color="var(--fg-muted)">
|
||||
{#if $current.membership === "ban"}
|
||||
{#if $current.member.power < 0}{rnd(["violently", "thoroughly", "briskly"])} {/if}
|
||||
{$current.powerLevel > 0 ? "fired" : rnd(["banned", "yeeted", "purged", "banished", "eliminated", "neutralized", "exiled", "expelled", "discontinued", "abolished", "discharged", "vanquished", "ejected"])}
|
||||
{:else}
|
||||
invited
|
||||
{/if}
|
||||
{formatDate($current.member.date, true)}
|
||||
{#if $current.membership === "ban"}
|
||||
{#if $current.member.power < 0}{rnd(["violently", "thoroughly", "briskly"])} {/if}
|
||||
{$current.powerLevel > 0 ? "fired" : rnd(["banned", "yeeted", "purged", "banished", "eliminated", "neutralized", "exiled", "expelled", "discontinued", "abolished", "discharged", "vanquished", "ejected"])}
|
||||
{:else}
|
||||
invited
|
||||
{/if}
|
||||
{formatDate($current.member.date, true)}
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
|
@ -177,7 +179,7 @@ pre {
|
|||
{#if $current.membership === "ban"}
|
||||
<Button type="danger" label="Unban" clicked={() => state.popup.set({ id: "todo" })} />
|
||||
{:else}
|
||||
<Button type="danger" label="Ban" clicked={() => state.popup.set({ id: "todo" })} />
|
||||
<Button type="danger" label="Ban" clicked={() => state.popup.set({ id: "ban", member: $current.member, room: $current.room })} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -235,5 +237,23 @@ pre {
|
|||
<h2 slot="header">todo <div class="close" on:click={closePopup}>×</div></h2>
|
||||
<p slot="content">nice, you found a todo popup</p>
|
||||
</Popup>
|
||||
{:else if $current.id === "ban"}
|
||||
<Popup>
|
||||
<h2 slot="header">Ban {$current.member.name}?</h2>
|
||||
<div slot="content">
|
||||
<div class="title">Ban scope</div>
|
||||
<Dropdown options={[
|
||||
["Only this room", "room"],
|
||||
["This space", "space"],
|
||||
// ["This server", "server"],
|
||||
]} />
|
||||
<div class="title">Reason for ban</div>
|
||||
<Textarea />
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<Button type="link" label="Nevermind!" clicked={closePopup} />
|
||||
<Button type="danger" label="Do it!" clicked={() => state.popup.set({ id: "todo" })} />
|
||||
</div>
|
||||
</Popup>
|
||||
{/if}
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
|
|
@ -19,6 +19,10 @@ button {
|
|||
transition: all 170ms;
|
||||
}
|
||||
|
||||
:not(.link) {
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.standard::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
46
src/ui/atoms/Dropdown.svelte
Normal file
46
src/ui/atoms/Dropdown.svelte
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script>
|
||||
export let options;
|
||||
export let selected = options[0];
|
||||
export let changed = () => {};
|
||||
let show = false;
|
||||
|
||||
function select(option) {
|
||||
selected = option;
|
||||
changed(option);
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 8px 0;
|
||||
padding: 10px;
|
||||
background: var(--bg-rooms-members);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: rgba(4,4,5,0.07);
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
background: #40444b;
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<div class="dropdown" on:click={() => show = true} on:blur={() => show = false}>
|
||||
{options.find(i => i[1] === selected)?.[0]}
|
||||
</div>
|
||||
{#if show}
|
||||
<div class="options">
|
||||
{#each options as option}
|
||||
<div class="option" class:selected={selected === option[1]} on:click={() => select(option[1])}>
|
||||
<div class="label">
|
||||
{option[0]}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -2,6 +2,7 @@
|
|||
// should only be used in src/Popups.svelte
|
||||
import { quadOut } from 'svelte/easing';
|
||||
export let raw = false;
|
||||
let current = state.popup;
|
||||
|
||||
function card() {
|
||||
return {
|
||||
|
@ -73,7 +74,7 @@ function opacity() {
|
|||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
<div class="background" on:click={() => state.popup.set({})} transition:opacity>
|
||||
<div class="background" on:click={() => state.popup.set({ ...$current, id: null })} transition:opacity>
|
||||
<div class="card" on:click={e => e.stopPropagation()} class:raw transition:card>
|
||||
{#if raw}
|
||||
<slot name="content"></slot>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
export let fetchBackwards = async () => {};
|
||||
export let fetchForwards = async () => {};
|
||||
export let getDefault = () => {};
|
||||
export let scrollTop, scrollMax;
|
||||
export let items, itemKey;
|
||||
export let margin = 400;
|
||||
|
@ -12,8 +13,7 @@ let atItemsTop, atItemsBottom;
|
|||
let paginating = false;
|
||||
|
||||
export function reset() {
|
||||
atItemsTop = direction === "up";
|
||||
atItemsBottom = direction === "down";
|
||||
[atItemsTop, atItemsBottom] = getDefault();
|
||||
if (direction === "down") {
|
||||
queueMicrotask(() => scrollEl && (scrollEl.scrollTop = scrollEl.scrollHeight));
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ function isRead(room) {
|
|||
if (index === -1) return null;
|
||||
for (let i = index; i >= 0; i--) {
|
||||
const event = state.events.get(timeline[i]);
|
||||
if (event.type === "m.room.message" && event.special !== "redacted") return event.eventId;
|
||||
if (["m.room.message", "m.room.create"].includes(event.type) && event.special !== "redacted") return event.eventId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -145,9 +145,11 @@ function isRead(room) {
|
|||
<div class="name">{room.name.toLowerCase().replace(/ /g, "-")}</div>
|
||||
<div class="spacer"></div>
|
||||
{#if room.pings}<div class="pings">{room.pings}</div>{/if}
|
||||
{#if room.power.me >= room.power.getBase("invite") || room.joinRule === "public"}
|
||||
<div class="settings" on:click={(e) => { e.stopImmediatePropagation(); state.popup.set({ id: "invite", type: "room", room }) }}>
|
||||
<Tooltip tip="Send Invite">🫂</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="settings" on:click={(e) => { e.stopImmediatePropagation(); state.selectedRoom.set(room); state.scene.set("room-settings") }}>
|
||||
<Tooltip tip="Edit Room">🔧</Tooltip>
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,7 @@ function isRead(room) {
|
|||
if (index === -1) return null;
|
||||
for (let i = index; i >= 0; i--) {
|
||||
const event = state.events.get(timeline[i]);
|
||||
if (event.type === "m.room.message" && event.special !== "redacted") return event.eventId;
|
||||
if (["m.room.message", "m.room.create"].includes(event.type) && event.special !== "redacted") return event.eventId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -15,10 +15,14 @@ async function create() {
|
|||
const { room_id } = await state.api.createRoom({
|
||||
name,
|
||||
creator: state.userId,
|
||||
...($current.type === "space" && { type: "m.space" }),
|
||||
});
|
||||
state.focusedRoomId = room_id;
|
||||
state.focusedRoom.set(state.rooms.get(room_id));
|
||||
state.popup.set({ type: $current.type });
|
||||
const interval = setInterval(() => {
|
||||
if (!state.rooms.has(room_id)) return;
|
||||
actions._rooms.focus(state.rooms.get(room_id));
|
||||
state.popup.set({ type: $current.type });
|
||||
clearInterval(interval);
|
||||
}, 10);
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
@ -35,13 +39,13 @@ async function create() {
|
|||
}
|
||||
</style>
|
||||
<Popup>
|
||||
<h3 slot="header">Create {capitalize($current.type)} <div class="close" on:click={() => state.popup.set({ type: $current.type })}>×</div></h3>
|
||||
<h3 slot="header">Create {capitalize($current.type)} <div class="close" on:click={() => state.popup.set({ id: null, ...$current })}>×</div></h3>
|
||||
<div slot="content" style="display: flex; flex-direction: column">
|
||||
<span class="title">{capitalize($current.type)} Name</span>
|
||||
<Input placeholder="awesome-{$current.type}" bind:value={name} submitted={create} autofocus />
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<Button type="link" label="Cancel" clicked={() => state.popup.set({ type: $current.type })} />
|
||||
<Button type="link" label="Cancel" clicked={() => state.popup.set({ id: null, ...$current })} />
|
||||
<Button type="primary" disabled={!name.length} label="Create {capitalize($current.type)}" clicked={create} />
|
||||
</div>
|
||||
</Popup>
|
||||
|
|
|
@ -23,11 +23,11 @@ function leave() {
|
|||
{#if $current.room.joinRule !== "public"}
|
||||
You won't be able to rejoin this {$current.type} unless you're re-invited.
|
||||
{:else}
|
||||
This {$current.type} is public and can be rejoined at any time. (Unless it gets made private!)
|
||||
This {$current.type} is public and can be rejoined at any time. (Unless it's made private!)
|
||||
{/if}
|
||||
</p>
|
||||
<div slot="footer">
|
||||
<Button type="link" label="Cancel" clicked={() => state.popup.set({ room: $current.room })} />
|
||||
<Button type="link" label="Cancel" clicked={() => state.popup.set({ ...$current, id: null })} />
|
||||
<Button type="danger" label="Leave {capitalize($current.type)}" clicked={leave} />
|
||||
</div>
|
||||
</Popup>
|
||||
|
|
|
@ -31,22 +31,20 @@ function dividerProps(prev, ev) {
|
|||
};
|
||||
}
|
||||
|
||||
function atBottom() {
|
||||
return $slice.atEnd();
|
||||
}
|
||||
|
||||
async function fetchBackwards() {
|
||||
const success = await actions.slice.backwards();
|
||||
return [!success || $slice.events[0]?.type === "m.room.create", atBottom()];
|
||||
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", atBottom()];
|
||||
return [!success || $slice.events[0]?.type === "m.room.create", $slice.atEnd()];
|
||||
}
|
||||
|
||||
function refocus() {
|
||||
if (scrollTo && scrollTop === scrollMax) queueMicrotask(() => scrollTo(-1));
|
||||
if (scrollTo && scrollTop === scrollMax) {
|
||||
queueMicrotask(() => scrollTo(-1));
|
||||
}
|
||||
}
|
||||
|
||||
let resizeTimeout;
|
||||
|
@ -80,6 +78,7 @@ function checkShift(e) {
|
|||
|
||||
onDestroy(state.focusedRoom.subscribe(() => reset && reset()));
|
||||
onDestroy(slice.subscribe(refocus));
|
||||
onDestroy(upload.subscribe(refocus));
|
||||
onDestroy(reply.subscribe(refocus));
|
||||
onDestroy(edit.subscribe(refocus));
|
||||
onDestroy(focused.subscribe(() => {
|
||||
|
@ -91,6 +90,13 @@ onDestroy(focused.subscribe(() => {
|
|||
setTimeout(() => id === $focused && focused.set(null), 2000);
|
||||
}
|
||||
}));
|
||||
onDestroy(edit.subscribe(() => {
|
||||
if (!$edit) return;
|
||||
const element = document.querySelector(`[data-event-id="${$edit}"]`);
|
||||
if (element) {
|
||||
setTimeout(() => element.scrollIntoView({ behavior: "smooth", block: "center" }));
|
||||
}
|
||||
}));
|
||||
</script>
|
||||
<style>
|
||||
.content {
|
||||
|
@ -174,6 +180,7 @@ onDestroy(focused.subscribe(() => {
|
|||
let:index={index}
|
||||
{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>
|
||||
|
|
|
@ -137,6 +137,7 @@ async function sendMessage(content, roomId = $room.roomId) {
|
|||
|
||||
onDestroy(state.focusedRoom.subscribe(() => queueMicrotask(() => textarea?.focus())));
|
||||
onDestroy(reply.subscribe(() => queueMicrotask(() => textarea?.focus())));
|
||||
onDestroy(edit.subscribe(() => queueMicrotask(() => $edit || textarea?.focus())));
|
||||
</script>
|
||||
<style>
|
||||
.container {
|
||||
|
|
|
@ -104,7 +104,6 @@ function handleClick(e) {
|
|||
.author {
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-right: 0.25rem;
|
||||
display: inline-block;
|
||||
height: 22px;
|
||||
}
|
||||
|
@ -113,6 +112,19 @@ function handleClick(e) {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0 4px;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.side {
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
@ -169,6 +181,10 @@ time {
|
|||
{#if header}
|
||||
<div class="top">
|
||||
<span class="author" style:color={getColor(sender)}>{sender.name || event.sender}</span>
|
||||
{#if event.content.msgtype === "m.notice"}
|
||||
<div class="badge">bot</div>
|
||||
{/if}
|
||||
<span style="margin-right: 0.25rem;"></span>
|
||||
<time datetime={event.date.toISOString()} style="display: inline">{formatDate(event.date)}</time>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -97,8 +97,7 @@ img {
|
|||
}
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
display: inline;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
@ -150,17 +149,17 @@ img {
|
|||
<div class="text" class:emote={type === "m.emote"}>
|
||||
{#if type === "m.emote"}*{/if}
|
||||
{@html parseHtml(content.formatted_body.trim()).trim()}
|
||||
{#if edited}
|
||||
<span class="edited">(edited)</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if edited}
|
||||
<span class="edited">(edited)</span>
|
||||
{/if}
|
||||
{:else if content.body}
|
||||
<div class="text" class:emote={type === "m.emote"}>
|
||||
{#if type === "m.emote"}*{/if}
|
||||
{@html parseHtml(content.body.replace(/&/g, "&").replace(/>/g, ">").replace(/</g, "<"), { linkify: true })}
|
||||
{#if edited}
|
||||
<span class="edited">(edited)</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if edited}
|
||||
<span class="edited">(edited)</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { marked } from "marked";
|
||||
import Button from "../../atoms/Button.svelte";
|
||||
|
||||
export let event;
|
||||
let textarea;
|
||||
|
@ -24,6 +23,7 @@ async function handleKeyDown(e) {
|
|||
} else if (e.key === "Escape") {
|
||||
$edit = null;
|
||||
} if (e.key === "ArrowUp") {
|
||||
if (input !== event.content.body) return;
|
||||
if (textarea.selectionStart !== 0) return;
|
||||
if (textarea.selectionEnd !== 0) return;
|
||||
for (let i = $slice.events.findIndex(i => i.eventId === $edit) - 1; i >= 0; i--) {
|
||||
|
@ -34,6 +34,7 @@ async function handleKeyDown(e) {
|
|||
}
|
||||
}
|
||||
} if (e.key === "ArrowDown") {
|
||||
if (input !== event.content.body) return;
|
||||
if (textarea.selectionStart !== input.length) return;
|
||||
if (textarea.selectionEnd !== input.length) return;
|
||||
for (let i = $slice.events.findIndex(i => i.eventId === $edit) + 1; i < $slice.events.length; i++) {
|
||||
|
@ -43,6 +44,7 @@ async function handleKeyDown(e) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
$edit = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +53,9 @@ function handleInput() {
|
|||
}
|
||||
|
||||
function save() {
|
||||
if (!input.trim()) {
|
||||
// TODO: delete message
|
||||
}
|
||||
const newEvent = {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.replace",
|
||||
|
|
|
@ -8,12 +8,14 @@ import Nitor from '../settings/user/Nitor.svelte';
|
|||
|
||||
import Appearance from '../settings/user/Appearance.svelte';
|
||||
// Text and Images
|
||||
import RoomsSpaces from '../settings/user/RoomsSpaces.svelte';
|
||||
import Notifications from '../settings/user/Notifications.svelte';
|
||||
import Keybinds from '../settings/user/Keybinds.svelte';
|
||||
// Language
|
||||
|
||||
import Changelog from '../settings/user/Changelog.svelte';
|
||||
import Help from '../settings/user/Help.svelte';
|
||||
import Version from '../settings/user/Version.svelte';
|
||||
import Testbed from '../settings/user/Testbed.svelte'; // temporary
|
||||
|
||||
const views = [
|
||||
|
@ -23,6 +25,7 @@ const views = [
|
|||
{ view: Nitor, name: "Nitor", color: "#ff73fa", raw: true },
|
||||
null,
|
||||
{ view: Appearance, name: "Appearance" },
|
||||
{ view: RoomsSpaces, name: "Rooms and Spaces" },
|
||||
{ view: null, name: "Text and Images" },
|
||||
{ view: Notifications, name: "Notifications" },
|
||||
{ view: Keybinds, name: "Keybinds" },
|
||||
|
@ -31,6 +34,7 @@ const views = [
|
|||
{ view: Testbed, name: "Testbed" },
|
||||
{ view: Changelog, name: "Changelog" },
|
||||
{ view: Help, name: "Help" },
|
||||
{ view: Version, name: "Credits" },
|
||||
null,
|
||||
{ name: "Log Out", color: "var(--color-red)", clicked: () => state.popup.set({ id: "logout", confirm: actions.client.logout }) },
|
||||
];
|
||||
|
|
|
@ -24,6 +24,7 @@ function showPopup(member) {
|
|||
id: "member",
|
||||
membership,
|
||||
member,
|
||||
room: $room,
|
||||
canBan: power.me > power.getUser(member.userId) && power.me >= power.getBase("ban"),
|
||||
canKick: power.me > power.getUser(member.userId) && power.me >= power.getBase("kick"),
|
||||
canInvite: power.me > power.getUser(member.userId) && power.me >= power.getBase("invite"),
|
||||
|
|
|
@ -1 +1,16 @@
|
|||
<script>
|
||||
export let room;
|
||||
const children = $room.state
|
||||
.filter(i => i.type === "m.space.child")
|
||||
.filter(i => Object.keys(i.content).length)
|
||||
</script>
|
||||
<p>todo: list rooms, create rooms, remove rooms</p>
|
||||
<br />
|
||||
<table>
|
||||
<tr><th>id</th><th>suggested</th></tr>
|
||||
{#each children as child}
|
||||
<tr><td>{child.state_key}</td><td>{child.content.suggested}</td></tr>
|
||||
{:else}
|
||||
<tr><i>no rooms</i></tr>
|
||||
{/each}
|
||||
</table>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script>
|
||||
import Radio from "../../atoms/Radio.svelte";
|
||||
import Toggle from "../../atoms/Toggle.svelte";
|
||||
// TODO: live update settings
|
||||
const opts = state.settings;
|
||||
</script>
|
||||
<style>
|
||||
.title {
|
||||
|
@ -11,6 +13,12 @@ import Radio from "../../atoms/Radio.svelte";
|
|||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
<div class="preview">
|
||||
<!-- TODO: preview message -->
|
||||
|
@ -18,8 +26,25 @@ import Radio from "../../atoms/Radio.svelte";
|
|||
<div class="title">User colors</div>
|
||||
<Radio
|
||||
options={[["Always", "always"], ["If they have a power level", "power"], ["Never", "never"]]}
|
||||
selected={state.settings.get("namecolors")}
|
||||
changed={(op) => state.settings.put("namecolors", op)}
|
||||
selected={opts.get("namecolors")}
|
||||
changed={(op) => opts.put("namecolors", op)}
|
||||
/>
|
||||
<br>
|
||||
<p>todo, low priority: show/hide join, leave, invite, nick/avatar change, room name/topic change</p>
|
||||
<br />
|
||||
<div class="title">Room timeline</div>
|
||||
<div class="option">
|
||||
<b>Show join and leaves</b>
|
||||
<Toggle checked={opts.get("showjoinleave")} toggled={(val) => opts.put("showjoinleave", val)}/>
|
||||
</div>
|
||||
<div class="option">
|
||||
<b>Show invites, kicks, bans, and unbans</b>
|
||||
<Toggle checked={opts.get("showmembership")} toggled={(val) => opts.put("showmembership", val)}/>
|
||||
</div>
|
||||
<div class="option">
|
||||
<b>Show name and avatar changes</b>
|
||||
<Toggle checked={opts.get("shownickavatar")} toggled={(val) => opts.put("shownickavatar", val)}/>
|
||||
</div>
|
||||
<div class="option">
|
||||
<b>Show room name and topic changes</b>
|
||||
<Toggle checked={opts.get("shownametopic")} toggled={(val) => opts.put("shownametopic", val)}/>
|
||||
</div>
|
||||
<br />
|
||||
|
|
|
@ -4,4 +4,4 @@ h2 {
|
|||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<p>you're using a pre-alpha build! nothing will be here until the first version is released</p>
|
||||
<p>you're using a pre-alpha {"bui"+"ld"}! nothing will be here until the first version is released</p>
|
||||
|
|
18
src/ui/settings/user/RoomsSpaces.svelte
Normal file
18
src/ui/settings/user/RoomsSpaces.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script>
|
||||
import Toggle from "../../atoms/Toggle.svelte";
|
||||
// TODO: live update settings
|
||||
</script>
|
||||
<style>
|
||||
.setting {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.setting > div {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<div class="setting">
|
||||
<div>Automatically join suggested rooms</div>
|
||||
<Toggle checked={state.settings.get("autojoin")} toggled={(val) => state.settings.put("autojoin", val)} />
|
||||
</div>
|
|
@ -1,8 +1,28 @@
|
|||
<script>
|
||||
import Toggle from "../../atoms/Toggle.svelte";
|
||||
import emojis from "emojibase-data/en/compact.json";
|
||||
import Input from "../../atoms/Input.svelte";
|
||||
const groups = [[], [], null, [], [], [], [], [], [], []];
|
||||
let selected;
|
||||
|
||||
console.log(state.settings);
|
||||
for (let emoji of emojis) {
|
||||
if (emoji.group === 2) continue;
|
||||
groups[emoji.group ?? 8].push(emoji);
|
||||
}
|
||||
|
||||
function getGroupName(id) {
|
||||
switch(id) {
|
||||
case 0: return "Smilies";
|
||||
case 1: return "People";
|
||||
case 2: return "???";
|
||||
case 3: return "Animals & nature";
|
||||
case 4: return "Food & Drink";
|
||||
case 5: return "Travel & Places";
|
||||
case 6: return "Activities";
|
||||
case 7: return "Objects";
|
||||
case 8: return "Symbols";
|
||||
case 9: return "Flags";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.selector {
|
||||
|
@ -18,12 +38,27 @@ console.log(state.settings);
|
|||
}
|
||||
|
||||
.emojis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 40px);
|
||||
background: var(--bg-rooms-members);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 40px);
|
||||
}
|
||||
|
||||
.emojis .label {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 8px;
|
||||
|
||||
background: var(--bg-rooms-members);
|
||||
font-family: var(--font-display);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
|
@ -39,6 +74,12 @@ console.log(state.settings);
|
|||
background: var(--color-gray-light);
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
background: var(--bg-misc);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-column: 1/3;
|
||||
background: var(--bg-rooms-members);
|
||||
|
@ -95,18 +136,29 @@ console.log(state.settings);
|
|||
</style>
|
||||
<p>testbed for random stuff</p>
|
||||
<br>
|
||||
<Toggle />
|
||||
<div class="selector">
|
||||
<div class="header">
|
||||
<Input small placeholder="search do it you wont" />
|
||||
</div>
|
||||
<div style="grid-row: 2/4; background: var(--bg-spaces)"></div>
|
||||
<div class="emojis">
|
||||
{#each new Array(100) as _, i}
|
||||
<div class="emoji">{String.fromCodePoint(i + 127744)}</div>
|
||||
{#each groups as emojis, i}
|
||||
{#if emojis}
|
||||
<div class="label">{getGroupName(i)}</div>
|
||||
<div class="group">
|
||||
{#each emojis as emoji}
|
||||
<div class="emoji" on:mouseover={() => selected = emoji}>{emoji.unicode}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div style="background: var(--bg-misc)"></div>
|
||||
<div class="preview">
|
||||
{#if selected}
|
||||
<div class="preview">{selected?.unicode}</div>
|
||||
<b>{selected.label}</b>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="popout">
|
||||
|
|
20
src/ui/settings/user/Version.svelte
Normal file
20
src/ui/settings/user/Version.svelte
Normal file
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
// TODO: this looks horrible, add styling and stuff later
|
||||
</script>
|
||||
{build.package.name} version {build.package.version}, commit <a href="https://git.celery.eu.org/tezlm/discard/commit/{build.commit}">{build.commit}</a>
|
||||
<br>
|
||||
<br>
|
||||
made with the following dependencies:
|
||||
<ul>
|
||||
{#each Object.entries(build.package.dependencies) as [pkg, ver]}
|
||||
<li><a href="https://npmjs.com/package/{pkg}">{pkg}</a> version {ver.replace(/^\^/, "")}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<br>
|
||||
along with the following, for development:
|
||||
<ul>
|
||||
{#each Object.entries(build.package.devDependencies) as [pkg, ver]}
|
||||
<li><a href="https://npmjs.com/package/{pkg}">{pkg}</a> version {ver.replace(/^\^/, "")}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
|
@ -84,14 +84,18 @@ function showPopup(id, opts) {
|
|||
<span>{$focusedSpace ? $focusedSpace?.name ?? "unknown" : "Home"}</span>
|
||||
{#if $focusedSpace && showMenu}
|
||||
<div class="menu" transition:zoomIn>
|
||||
{#if $focusedSpace.power.me >= $focusedSpace.power.getBase("invite") || $focusedSpace.joinRule === "public"}
|
||||
<div class="item" on:click={() => showPopup("invite")}><span class="color-accent">Invite People</span></div>
|
||||
<div class="spacer"></div>
|
||||
{/if}
|
||||
<div class="item" on:click={() => { state.selectedRoom.set($focusedSpace); state.scene.set("space-settings") }}>Space Settings</div>
|
||||
<div class="spacer"></div>
|
||||
{#if $focusedSpace.power.me >= $focusedSpace.power.getState("m.space.child")}
|
||||
<div class="item" on:click={() => showPopup("create", { type: "room", confirm: todo })}>Create Room</div>
|
||||
<div class="item" on:click={() => showPopup("create", { confirm: todo })}>Create Subspace</div>
|
||||
<div class="item" on:click={todo}>Add Existing Room</div>
|
||||
<div class="spacer"></div>
|
||||
{/if}
|
||||
<div class="item" on:click={() => showPopup("leave", { confirm: () => queueMicrotask(todo) })}><span class="color-red">Leave Space</span></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -4,7 +4,7 @@ import sanitizeHtml from "sanitize-html";
|
|||
import { parseMxc } from "./content.js";
|
||||
|
||||
const permittedHtmlTagsInline = [
|
||||
"font", "del", "p", "a", "sup", "sub", "b", "i",
|
||||
"font", "del", /*"p", */ "a", "sup", "sub", "b", "i",
|
||||
"u", "strong", "em", "strike", "code", "br", "span",
|
||||
];
|
||||
|
||||
|
@ -88,6 +88,7 @@ const sanitizeOpts = {
|
|||
span: transformFontSpanTags,
|
||||
a: transformATag,
|
||||
img: transformImgTag,
|
||||
p: "span",
|
||||
},
|
||||
nonTextTags: ["style", "script", "textarea", "option", "noscript", "mx-reply"],
|
||||
nestingLimit: 100,
|
||||
|
@ -100,6 +101,7 @@ const sanitizeOptsInline = {
|
|||
font: transformFontSpanTags,
|
||||
span: transformFontSpanTags,
|
||||
a: transformATag,
|
||||
p: "span",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ export function formatRoom(roomId, stateEvents, accountData) {
|
|||
}
|
||||
|
||||
function getPower() {
|
||||
const power = getState("m.room.power_levels") ?? { state_default: 0 };
|
||||
const power = getState("m.room.power_levels") ?? {};
|
||||
power.me = power.users?.[state.userId] ?? power.users_default ?? 0;
|
||||
return {
|
||||
...power,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { defineConfig } from "vite"
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import { execSync } from "child_process";
|
||||
import pkgjson from "./package.json" assert { type: "json" };
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
@ -10,4 +12,11 @@ export default defineConfig({
|
|||
outDir: "../dist",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
define: {
|
||||
build: {
|
||||
package: pkgjson,
|
||||
time: new Date(),
|
||||
commit: execSync("git log -n 1 --oneline HEAD").toString().match(/[a-z0-9]+/)[0],
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
Reference in a new issue