init: initial commit
This commit is contained in:
		
							
								
								
									
										13
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					/build
 | 
				
			||||||
 | 
					/.svelte-kit
 | 
				
			||||||
 | 
					/package
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					.env.*
 | 
				
			||||||
 | 
					!.env.example
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Ignore files for PNPM, NPM and YARN
 | 
				
			||||||
 | 
					pnpm-lock.yaml
 | 
				
			||||||
 | 
					package-lock.json
 | 
				
			||||||
 | 
					yarn.lock
 | 
				
			||||||
							
								
								
									
										20
									
								
								.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
						root: true,
 | 
				
			||||||
 | 
						parser: '@typescript-eslint/parser',
 | 
				
			||||||
 | 
						extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
 | 
				
			||||||
 | 
						plugins: ['svelte3', '@typescript-eslint'],
 | 
				
			||||||
 | 
						ignorePatterns: ['*.cjs'],
 | 
				
			||||||
 | 
						overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
 | 
				
			||||||
 | 
						settings: {
 | 
				
			||||||
 | 
							'svelte3/typescript': () => require('typescript')
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parserOptions: {
 | 
				
			||||||
 | 
							sourceType: 'module',
 | 
				
			||||||
 | 
							ecmaVersion: 2020
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						env: {
 | 
				
			||||||
 | 
							browser: true,
 | 
				
			||||||
 | 
							es2017: true,
 | 
				
			||||||
 | 
							node: true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					/build
 | 
				
			||||||
 | 
					/.svelte-kit
 | 
				
			||||||
 | 
					/package
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					.env.*
 | 
				
			||||||
 | 
					!.env.example
 | 
				
			||||||
 | 
					.log/
 | 
				
			||||||
 | 
					vite.config.js.timestamp-*
 | 
				
			||||||
 | 
					vite.config.ts.timestamp-*
 | 
				
			||||||
							
								
								
									
										13
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					/build
 | 
				
			||||||
 | 
					/.svelte-kit
 | 
				
			||||||
 | 
					/package
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					.env.*
 | 
				
			||||||
 | 
					!.env.example
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Ignore files for PNPM, NPM and YARN
 | 
				
			||||||
 | 
					pnpm-lock.yaml
 | 
				
			||||||
 | 
					package-lock.json
 | 
				
			||||||
 | 
					yarn.lock
 | 
				
			||||||
							
								
								
									
										10
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"useTabs": false,
 | 
				
			||||||
 | 
						"tabWidth": 2,
 | 
				
			||||||
 | 
						"singleQuote": true,
 | 
				
			||||||
 | 
						"trailingComma": "none",
 | 
				
			||||||
 | 
						"printWidth": 100,
 | 
				
			||||||
 | 
						"plugins": ["prettier-plugin-svelte"],
 | 
				
			||||||
 | 
						"pluginSearchDirs": ["."],
 | 
				
			||||||
 | 
						"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					# create-svelte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Creating a project
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you're seeing this, you've probably already done this step. Congrats!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					# create a new project in the current directory
 | 
				
			||||||
 | 
					npm create svelte@latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# create a new project in my-app
 | 
				
			||||||
 | 
					npm create svelte@latest my-app
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Developing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					npm run dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# or start the server and open the app in a new browser tab
 | 
				
			||||||
 | 
					npm run dev -- --open
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Building
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To create a production version of your app:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					npm run build
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can preview the production build with `npm run preview`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
 | 
				
			||||||
							
								
								
									
										3159
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3159
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										44
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"name": "litechatplus",
 | 
				
			||||||
 | 
						"version": "0.0.1",
 | 
				
			||||||
 | 
						"private": true,
 | 
				
			||||||
 | 
						"scripts": {
 | 
				
			||||||
 | 
							"dev": "vite dev",
 | 
				
			||||||
 | 
							"build": "vite build",
 | 
				
			||||||
 | 
							"preview": "vite preview",
 | 
				
			||||||
 | 
							"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
 | 
				
			||||||
 | 
							"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
 | 
				
			||||||
 | 
							"lint": "prettier --plugin-search-dir . --check . && eslint .",
 | 
				
			||||||
 | 
							"format": "prettier --plugin-search-dir . --write ."
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"devDependencies": {
 | 
				
			||||||
 | 
							"@sveltejs/adapter-auto": "^1.0.0",
 | 
				
			||||||
 | 
							"@sveltejs/kit": "^1.0.0",
 | 
				
			||||||
 | 
							"@typescript-eslint/eslint-plugin": "^5.45.0",
 | 
				
			||||||
 | 
							"@typescript-eslint/parser": "^5.45.0",
 | 
				
			||||||
 | 
							"eslint": "^8.28.0",
 | 
				
			||||||
 | 
							"eslint-config-prettier": "^8.5.0",
 | 
				
			||||||
 | 
							"eslint-plugin-svelte3": "^4.0.0",
 | 
				
			||||||
 | 
							"prettier": "^2.8.0",
 | 
				
			||||||
 | 
							"prettier-plugin-svelte": "^2.9.0",
 | 
				
			||||||
 | 
							"sass": "^1.58.0",
 | 
				
			||||||
 | 
							"svelte": "^3.54.0",
 | 
				
			||||||
 | 
							"svelte-check": "^3.0.1",
 | 
				
			||||||
 | 
							"svelte-floating-ui": "^1.2.8",
 | 
				
			||||||
 | 
							"svelte-virtual-scroll-list": "^1.1.0",
 | 
				
			||||||
 | 
							"tslib": "^2.4.1",
 | 
				
			||||||
 | 
							"typescript": "^4.9.3",
 | 
				
			||||||
 | 
							"vite": "^4.0.0"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"type": "module",
 | 
				
			||||||
 | 
						"dependencies": {
 | 
				
			||||||
 | 
							"@fortawesome/fontawesome-free": "^6.2.1",
 | 
				
			||||||
 | 
							"@fortawesome/fontawesome-svg-core": "^6.2.1",
 | 
				
			||||||
 | 
							"@fortawesome/free-solid-svg-icons": "^6.2.1",
 | 
				
			||||||
 | 
							"@fortawesome/svelte-fontawesome": "^0.2.0",
 | 
				
			||||||
 | 
							"detect-it": "^4.0.1",
 | 
				
			||||||
 | 
							"fedwave-chat-client": "^0.0.1",
 | 
				
			||||||
 | 
							"jose": "^4.11.2",
 | 
				
			||||||
 | 
							"svrollbar": "^0.12.0"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					// See https://kit.svelte.dev/docs/types#app
 | 
				
			||||||
 | 
					// for information about these interfaces
 | 
				
			||||||
 | 
					// and what to do when importing types
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  const __COMMIT__: string;
 | 
				
			||||||
 | 
					  namespace App {
 | 
				
			||||||
 | 
					    // interface Error {}
 | 
				
			||||||
 | 
					    // interface Locals {}
 | 
				
			||||||
 | 
					    // interface PageData {}
 | 
				
			||||||
 | 
					    // interface Platform {}
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {};
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/app.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/app.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
						<head>
 | 
				
			||||||
 | 
							<meta charset="utf-8" />
 | 
				
			||||||
 | 
							<link rel="icon" href="%sveltekit.assets%/favicon.png" />
 | 
				
			||||||
 | 
							<meta name="viewport" content="width=device-width" />
 | 
				
			||||||
 | 
							%sveltekit.head%
 | 
				
			||||||
 | 
							<style>
 | 
				
			||||||
 | 
							  html, body {
 | 
				
			||||||
 | 
							      height: 100%;
 | 
				
			||||||
 | 
							  }
 | 
				
			||||||
 | 
							</style>
 | 
				
			||||||
 | 
						</head>
 | 
				
			||||||
 | 
						<body data-sveltekit-preload-data="hover" style="margin: 0;" class="solarized-light">
 | 
				
			||||||
 | 
							<div style="display: contents">%sveltekit.body%</div>
 | 
				
			||||||
 | 
						</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										44
									
								
								src/lib/About.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/lib/About.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import { version } from '$app/environment';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="about d-flex col no-select">
 | 
				
			||||||
 | 
					    <h1 class="litechat">litechat<span class="plus">plus</span></h1>
 | 
				
			||||||
 | 
					    <p>A [<i>fedwave</i>] chat client.</p>
 | 
				
			||||||
 | 
					    <div class="d-flex build-info">
 | 
				
			||||||
 | 
						<p class="version">Version: {version}</p>
 | 
				
			||||||
 | 
						<div class="spacer" />
 | 
				
			||||||
 | 
						<p class="commit">{__COMMIT__}</p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
					 .about {
 | 
				
			||||||
 | 
					     height: var(--top-screen-cutout-height);
 | 
				
			||||||
 | 
					     width: var(--sidebar-width, 350px);
 | 
				
			||||||
 | 
					     text-align: center;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .litechat {
 | 
				
			||||||
 | 
					     color: var(--base-300);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     margin-bottom: 0px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     & .plus {
 | 
				
			||||||
 | 
						 vertical-align: super;
 | 
				
			||||||
 | 
						 color: var(--cyan);
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .build-info {
 | 
				
			||||||
 | 
					     padding-left: 24px;
 | 
				
			||||||
 | 
					     padding-right: 24px;
 | 
				
			||||||
 | 
					     & > * {
 | 
				
			||||||
 | 
						 user-select: text;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .version {
 | 
				
			||||||
 | 
					     color: var(--base-400);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/lib/Avatar.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/lib/Avatar.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 export let username: string = '';
 | 
				
			||||||
 | 
					 export let avatar: string = '/troll_haz2.png';
 | 
				
			||||||
 | 
					 export let color: string = 'var(--base-400)';
 | 
				
			||||||
 | 
					 export let size: string = 'var(--avatar-size)';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="avatar" style:--color="{color}" style:--size="{size}" on:click>
 | 
				
			||||||
 | 
					    <img src="{avatar}" alt="{`${username}'s avatar`}">
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					 .avatar, .avatar > img {
 | 
				
			||||||
 | 
					     width: var(--size);
 | 
				
			||||||
 | 
					     height: var(--size);
 | 
				
			||||||
 | 
					     border-radius: 9999px;
 | 
				
			||||||
 | 
					     overflow: hidden;
 | 
				
			||||||
 | 
					     background-color: var(--color);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										193
									
								
								src/lib/ChatMessage.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								src/lib/ChatMessage.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,193 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import type { Message } from "fedwave-chat-client";
 | 
				
			||||||
 | 
					 import { room, username } from "./chat";
 | 
				
			||||||
 | 
					 export let globalMode = false;
 | 
				
			||||||
 | 
					 export let message: Message;
 | 
				
			||||||
 | 
					 export let isHeader: boolean = true;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="message" class:subseq="{!isHeader}">
 | 
				
			||||||
 | 
					    {#if isHeader}
 | 
				
			||||||
 | 
						<div class="avatar" style="background-color: {message.color}">
 | 
				
			||||||
 | 
						    <img alt="{message.username}'s avatar" src="{message.avatar || '/troll_haz2.png'}">
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					    <div class="container">
 | 
				
			||||||
 | 
						{#if isHeader}
 | 
				
			||||||
 | 
						    <div class="top">
 | 
				
			||||||
 | 
							<div class="username">
 | 
				
			||||||
 | 
							    {message.username}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="badge">
 | 
				
			||||||
 | 
							    {message.badge || ""}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="timestamp">
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="spacer" />
 | 
				
			||||||
 | 
							{#if globalMode}
 | 
				
			||||||
 | 
							    <a href="/{message.channel}" class="channel"
 | 
				
			||||||
 | 
							       class:local="{$room == message.channel.toLowerCase()}">
 | 
				
			||||||
 | 
								{message.channel}
 | 
				
			||||||
 | 
							    </a>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
						<div class="text">
 | 
				
			||||||
 | 
						    {@html message.message}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					 :root {
 | 
				
			||||||
 | 
					     --message-line-height: 22px;
 | 
				
			||||||
 | 
					     --avatar-size: 35px;
 | 
				
			||||||
 | 
					     --greentext-color: #789922;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 * {
 | 
				
			||||||
 | 
					     font-size: 14px;
 | 
				
			||||||
 | 
					     font-optical-sizing: auto;
 | 
				
			||||||
 | 
					     font-family: IBM Plex Sans
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .message {
 | 
				
			||||||
 | 
					     position: relative;
 | 
				
			||||||
 | 
					     padding-left: 8px;
 | 
				
			||||||
 | 
					     padding-right: 24px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .message:last-child {
 | 
				
			||||||
 | 
					     margin-bottom: 12px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .message:hover {
 | 
				
			||||||
 | 
					     background: var(--base-700);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .avatar {
 | 
				
			||||||
 | 
					     border-radius: 9999px;
 | 
				
			||||||
 | 
					     overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     min-height: var(--avatar-size);
 | 
				
			||||||
 | 
					     min-width: var(--avatar-size);
 | 
				
			||||||
 | 
					     max-height: var(--avatar-size);
 | 
				
			||||||
 | 
					     max-width: var(--avatar-size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     user-select: none;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .avatar > img {
 | 
				
			||||||
 | 
					     width: 100%;
 | 
				
			||||||
 | 
					     height: auto
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .message:not(.subseq) {
 | 
				
			||||||
 | 
					     padding-top: 4px;
 | 
				
			||||||
 | 
					     margin-top: 8px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .subseq {
 | 
				
			||||||
 | 
					     margin-top: 0;
 | 
				
			||||||
 | 
					     padding-top: 0;
 | 
				
			||||||
 | 
					     padding-left: calc(16px + var(--avatar-size));
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .subseq > .container {
 | 
				
			||||||
 | 
					     margin-left: 0;
 | 
				
			||||||
 | 
					     padding-top: 2px;
 | 
				
			||||||
 | 
					     padding-bottom: 2px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .container {
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     flex-direction: column;
 | 
				
			||||||
 | 
					     justify-content: space-between;
 | 
				
			||||||
 | 
					     margin-left: 8px;
 | 
				
			||||||
 | 
					     width: 100%;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .top {
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     height: 18px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .top .spacer {
 | 
				
			||||||
 | 
					     flex-grow: 1;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .username {
 | 
				
			||||||
 | 
					     font-weight: bold;
 | 
				
			||||||
 | 
					     font-optical-sizing: auto;
 | 
				
			||||||
 | 
					     height: 12px;
 | 
				
			||||||
 | 
					     font-family: IBM Plex Mono, Consolas, monospace;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .channel {
 | 
				
			||||||
 | 
					     --background: var(--base-600);
 | 
				
			||||||
 | 
					     border-radius: 4px;
 | 
				
			||||||
 | 
					     background-color: var(--background);
 | 
				
			||||||
 | 
					     padding-left: 2px;
 | 
				
			||||||
 | 
					     padding-right: 2px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     text-align: right;
 | 
				
			||||||
 | 
					     font-family: IBM Plex Mono, Consolas, monospace;
 | 
				
			||||||
 | 
					     text-transform: lowercase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     color: var(--base-700);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     user-select: none;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .channel.local {
 | 
				
			||||||
 | 
					     --background: var(--cyan);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .text {
 | 
				
			||||||
 | 
					     flex-basis: 0;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .text :global(.mention) {
 | 
				
			||||||
 | 
					     font-weight: 800;
 | 
				
			||||||
 | 
					     color: var(--yellow);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .text :global(p:empty), .text :global(br:nth-last-child(1)), .text :global(br) {
 | 
				
			||||||
 | 
					     display: none;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .text > :global(blockquote) {
 | 
				
			||||||
 | 
					     margin: 0;
 | 
				
			||||||
 | 
					     color: var(--greentext-color);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .text > :global(p), .text > :global(blockquote) {
 | 
				
			||||||
 | 
					     line-height: var(--message-line-height);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .text :global(h1) {
 | 
				
			||||||
 | 
					     margin: 0;
 | 
				
			||||||
 | 
					     margin-top: 8px;
 | 
				
			||||||
 | 
					     margin-bottom: 8px;
 | 
				
			||||||
 | 
					     font-size: 1.5em;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .text :global(*) {
 | 
				
			||||||
 | 
					     overflow-wrap: anywhere;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .text :global(p) {
 | 
				
			||||||
 | 
					     margin: 0;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .text :global(img) {
 | 
				
			||||||
 | 
					     height: var(--message-line-height);
 | 
				
			||||||
 | 
					     vertical-align: bottom;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .text :global(h1 img) {
 | 
				
			||||||
 | 
					     height: calc(2 * var(--message-line-height));
 | 
				
			||||||
 | 
					     vertical-align: bottom;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										123
									
								
								src/lib/ChatMessages.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/lib/ChatMessages.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import FAB from './ui/FAB.svelte';
 | 
				
			||||||
 | 
					 import MessageView from './MessageView.svelte';
 | 
				
			||||||
 | 
					 import Input from './Input.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { Svrollbar } from 'svrollbar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { chat_lock, chat, localMessages as messages, shouldTweet } from './chat';
 | 
				
			||||||
 | 
					 import { globalMode } from './settings';
 | 
				
			||||||
 | 
					 import { onMount, onDestroy, tick } from 'svelte';
 | 
				
			||||||
 | 
					 import type { Unsubscriber } from 'svelte/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
 | 
				
			||||||
 | 
					 import { faArrowDown } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let stickToBottom = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let viewport: Element, contents: Element;
 | 
				
			||||||
 | 
					 let tweet: HTMLAudioElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 shouldTweet.subscribe((should) => {
 | 
				
			||||||
 | 
					     if (should) {
 | 
				
			||||||
 | 
						 if (tweet) {
 | 
				
			||||||
 | 
					             tweet.volume = 0.5;
 | 
				
			||||||
 | 
					             tweet?.play().catch(() => {});
 | 
				
			||||||
 | 
						 }
 | 
				
			||||||
 | 
						 shouldTweet.set(false);
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 export async function scrollToBottom(smooth = true) {
 | 
				
			||||||
 | 
					     viewport &&
 | 
				
			||||||
 | 
					     viewport.scrollTo({
 | 
				
			||||||
 | 
					         top: viewport.scrollHeight,
 | 
				
			||||||
 | 
					         behavior: smooth ? 'smooth' : undefined
 | 
				
			||||||
 | 
					     });
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let releaseLock: Unsubscriber;
 | 
				
			||||||
 | 
					 onMount(async () => {
 | 
				
			||||||
 | 
					     releaseLock = chat_lock.subscribe(() => {});
 | 
				
			||||||
 | 
					     await chat.hydrate();
 | 
				
			||||||
 | 
					     await tick();
 | 
				
			||||||
 | 
					     viewport && (viewport.scrollTop = viewport.scrollHeight);
 | 
				
			||||||
 | 
					 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 messages.subscribe(async () => {
 | 
				
			||||||
 | 
					     if (stickToBottom) {
 | 
				
			||||||
 | 
						 // Wait for the message to be added to the DOM
 | 
				
			||||||
 | 
						 await tick();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 // Scroll the list to the bottom with the new DOM
 | 
				
			||||||
 | 
						 await scrollToBottom(false);
 | 
				
			||||||
 | 
						 // vlist?.scrollToIndex(ms.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 // Fake debounce to make sure we're still stuck at the bottom
 | 
				
			||||||
 | 
						 await tick();
 | 
				
			||||||
 | 
						 stickToBottom = true;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 onDestroy(() => {
 | 
				
			||||||
 | 
					     releaseLock?.();
 | 
				
			||||||
 | 
					 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function onScroll(e: Event) {
 | 
				
			||||||
 | 
					     if (!e.isTrusted) return;
 | 
				
			||||||
 | 
					     const scrollOffset = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     if (scrollOffset >= 80) {
 | 
				
			||||||
 | 
						 stickToBottom = false;
 | 
				
			||||||
 | 
					     } else if (scrollOffset < 80) {
 | 
				
			||||||
 | 
						 stickToBottom = true;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="scroller">
 | 
				
			||||||
 | 
					    <audio src="/tweet.mp3" bind:this={tweet} />
 | 
				
			||||||
 | 
					    <Svrollbar {viewport} {contents}  margin={{ right: 6, bottom: 12, top: 12 }}
 | 
				
			||||||
 | 
						       alwaysVisible
 | 
				
			||||||
 | 
						       --svrollbar-track-width="8px" --svrollbar-thumb-width="4px" />
 | 
				
			||||||
 | 
					  <MessageView messages="{$messages}" showChannel="{$globalMode}"
 | 
				
			||||||
 | 
						       bind:viewport bind:contents on:scroll={onScroll} />
 | 
				
			||||||
 | 
					  {#if !stickToBottom}
 | 
				
			||||||
 | 
					      <FAB --background="{'var(--base-700)'}" on:click={() => scrollToBottom()}><FontAwesomeIcon icon="{faArrowDown}" /></FAB>
 | 
				
			||||||
 | 
					  {/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<Input />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					  .scroller {
 | 
				
			||||||
 | 
					    min-height: 0;
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    flex-basis: 0;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .scroller > :global(.viewport) {
 | 
				
			||||||
 | 
					    overflow: scroll;
 | 
				
			||||||
 | 
					    height: 100% !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .scroller :global(.contents) {
 | 
				
			||||||
 | 
					    margin-bottom: 32px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .scroller > :global(*) {
 | 
				
			||||||
 | 
					     /* hide scrollbar */
 | 
				
			||||||
 | 
					     -ms-overflow-style: none !important;
 | 
				
			||||||
 | 
					     scrollbar-width: none !important;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .scroller > :global(*::-webkit-scrollbar) {
 | 
				
			||||||
 | 
					     /* hide scrollbar */
 | 
				
			||||||
 | 
					     display: none !important;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .scroller :global(.v-thumb) {
 | 
				
			||||||
 | 
					    z-index: 101;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										49
									
								
								src/lib/ChatScreen.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/lib/ChatScreen.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import Input from "$lib/Input.svelte";
 | 
				
			||||||
 | 
					 import Header from "$lib/Header.svelte";
 | 
				
			||||||
 | 
					 import Avatar from '$lib/Avatar.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { color, room, username } from "$lib/chat";
 | 
				
			||||||
 | 
					 import { primaryInput } from 'detect-it';
 | 
				
			||||||
 | 
					 import { onMount } from "svelte";
 | 
				
			||||||
 | 
					 import { tweened } from "svelte/motion";
 | 
				
			||||||
 | 
					 import { quintOut } from 'svelte/easing';
 | 
				
			||||||
 | 
					 import { fade } from 'svelte/transition';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let scrolledToTop = false, scrolledToBottom = false;
 | 
				
			||||||
 | 
					 let topHeight = 0;
 | 
				
			||||||
 | 
					 let container: Element;
 | 
				
			||||||
 | 
					 export let darkenAmount = 0;
 | 
				
			||||||
 | 
					 onMount(() => {
 | 
				
			||||||
 | 
					     container.scrollTop = topHeight;
 | 
				
			||||||
 | 
					 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function overlayClick() {
 | 
				
			||||||
 | 
					     if(scrolledToTop) {
 | 
				
			||||||
 | 
						 container.scrollTo({top: topHeight, behavior: 'smooth'});
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="chat screen">
 | 
				
			||||||
 | 
					    {#if primaryInput == 'mouse' && scrolledToTop}
 | 
				
			||||||
 | 
						<div class="overlay" transition:fade />
 | 
				
			||||||
 | 
					    {:else if primaryInput == 'touch'}
 | 
				
			||||||
 | 
						<div class="overlay"
 | 
				
			||||||
 | 
						     style:--darken-amount="{darkenAmount}"
 | 
				
			||||||
 | 
						     style:--should-display="{scrolledToBottom ? 'none' : 'unset'}"
 | 
				
			||||||
 | 
						     on:click="{overlayClick}" />
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					    <Header flex --header-color="var(--top-screen-background-color)">
 | 
				
			||||||
 | 
						<div class="vert-center room-name">#{$room}</div>
 | 
				
			||||||
 | 
						<div class="spacer"/>
 | 
				
			||||||
 | 
						<div class="vert-center avatar">
 | 
				
			||||||
 | 
						    <Avatar color="{$color}" username="{$username}" />
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					    </Header>
 | 
				
			||||||
 | 
					    <slot />
 | 
				
			||||||
 | 
					    <Input />
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										59
									
								
								src/lib/Header.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/lib/Header.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
					 export let flex = false;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="header" class:flex="{flex}">
 | 
				
			||||||
 | 
					    <slot />
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					 :global(body) {
 | 
				
			||||||
 | 
					     --header-size: 48px;
 | 
				
			||||||
 | 
					     --header-color: var(--base-600);
 | 
				
			||||||
 | 
					     --header-z: 101;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .flex {
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     flex-direction: row;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .header {
 | 
				
			||||||
 | 
					     position: relative;
 | 
				
			||||||
 | 
					     height: var(--header-size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     /* box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     background-color: var(--header-color);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .header > :global(*) {
 | 
				
			||||||
 | 
					     z-index: calc(10 + var(--header-z));
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .header::before {
 | 
				
			||||||
 | 
					     content: "";
 | 
				
			||||||
 | 
					     position: absolute;
 | 
				
			||||||
 | 
					     left: 0;
 | 
				
			||||||
 | 
					     z-index: var(--header-z);
 | 
				
			||||||
 | 
					     background-color: transparent;
 | 
				
			||||||
 | 
					     bottom: -50px;
 | 
				
			||||||
 | 
					     height: 50px;
 | 
				
			||||||
 | 
					     width: 25px;
 | 
				
			||||||
 | 
					     border-top-left-radius: 25px;
 | 
				
			||||||
 | 
					     box-shadow: 0 -25px 0 0 var(--header-color);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .header::after {
 | 
				
			||||||
 | 
					     content: "";
 | 
				
			||||||
 | 
					     position: absolute;
 | 
				
			||||||
 | 
					     right: 0;
 | 
				
			||||||
 | 
					     z-index: var(--header-z);
 | 
				
			||||||
 | 
					     background-color: transparent;
 | 
				
			||||||
 | 
					     bottom: -50px;
 | 
				
			||||||
 | 
					     height: 50px;
 | 
				
			||||||
 | 
					     width: 25px;
 | 
				
			||||||
 | 
					     border-top-right-radius: 25px;
 | 
				
			||||||
 | 
					     box-shadow: 0 -25px 0 0 var(--header-color);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										57
									
								
								src/lib/Input.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/lib/Input.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import { tick } from "svelte"
 | 
				
			||||||
 | 
					 import { send } from "./chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let input = "";
 | 
				
			||||||
 | 
					 async function keydown(e: KeyboardEvent) {
 | 
				
			||||||
 | 
					     if(e.key === "Enter") {
 | 
				
			||||||
 | 
						 send(input);
 | 
				
			||||||
 | 
						 e.preventDefault();
 | 
				
			||||||
 | 
						 await tick();
 | 
				
			||||||
 | 
						 input = "";
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="container">
 | 
				
			||||||
 | 
					    <textarea bind:value="{input}" on:keydown={keydown} class="input" />
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					 .container {
 | 
				
			||||||
 | 
					     height: calc(2 * var(--message-line-height));
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     padding: 16px;
 | 
				
			||||||
 | 
					     padding-top: 0;
 | 
				
			||||||
 | 
					     margin-top: -12px;
 | 
				
			||||||
 | 
					     z-index: 100;
 | 
				
			||||||
 | 
					     position: relative
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .container::before, .container::after {
 | 
				
			||||||
 | 
					     content: "";
 | 
				
			||||||
 | 
					     z-index: 99;
 | 
				
			||||||
 | 
					     background: linear-gradient(180deg, rgba(var(--base-800-rgb),0) 0%, rgb(var(--base-800-rgb)) 75%);
 | 
				
			||||||
 | 
					     width: 24px;
 | 
				
			||||||
 | 
					     position: absolute;
 | 
				
			||||||
 | 
					     height: 16px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .container::before { left: 0;  }
 | 
				
			||||||
 | 
					 .container::after  { right: 0; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .input {
 | 
				
			||||||
 | 
					     flex-grow: 1;
 | 
				
			||||||
 | 
					     resize: none;
 | 
				
			||||||
 | 
					     border-radius: 16px;
 | 
				
			||||||
 | 
					     border: none;
 | 
				
			||||||
 | 
					     z-index: 100;
 | 
				
			||||||
 | 
					     padding: 8px;
 | 
				
			||||||
 | 
					     background: var(--base-700);
 | 
				
			||||||
 | 
					     color: var(--base-300);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .input:focus {
 | 
				
			||||||
 | 
					     outline: none;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/lib/MessageView.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/lib/MessageView.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import ChatMessage from "./ChatMessage.svelte";
 | 
				
			||||||
 | 
					 import type { TaggedMessage } from "./chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 export let showChannel: boolean = true;
 | 
				
			||||||
 | 
					 export let messages: TaggedMessage[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 export let viewport: Element = undefined as any as Element;
 | 
				
			||||||
 | 
					 export let contents: Element = undefined as any as Element;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="viewport" bind:this={viewport} on:scroll>
 | 
				
			||||||
 | 
					    <div class="contents" bind:this={contents}>
 | 
				
			||||||
 | 
						{#each messages as message}
 | 
				
			||||||
 | 
					            <ChatMessage message={message.m} isHeader={message.isHeader} globalMode={showChannel} />
 | 
				
			||||||
 | 
						{/each}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										80
									
								
								src/lib/Sidebar.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/lib/Sidebar.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 export let clientWidth = 0;
 | 
				
			||||||
 | 
					 export let mobile = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { FontAwesomeIcon as FA } from '@fortawesome/svelte-fontawesome';
 | 
				
			||||||
 | 
					 import { faHome, faPerson } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="sidebar h-100vh d-flex" bind:clientWidth class:mobile="{mobile}">
 | 
				
			||||||
 | 
					    <div class="overlay" />
 | 
				
			||||||
 | 
					    <div class="servers d-flex col">
 | 
				
			||||||
 | 
						<button class="selected">
 | 
				
			||||||
 | 
						    <FA size="2x" icon="{faPerson}" />
 | 
				
			||||||
 | 
						</button>
 | 
				
			||||||
 | 
						<button>
 | 
				
			||||||
 | 
						    <FA size="2x" icon="{faHome}" />
 | 
				
			||||||
 | 
						</button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
					 @use "sass:string";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 :root {
 | 
				
			||||||
 | 
					     --sidebar-server-select-width: 64px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .sidebar {
 | 
				
			||||||
 | 
					     z-index: 0;
 | 
				
			||||||
 | 
					     background: var(--base-600);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .sidebar:not(.mobile) {
 | 
				
			||||||
 | 
					     width: 350px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .mobile {
 | 
				
			||||||
 | 
					     width: 70vw;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .overlay {
 | 
				
			||||||
 | 
					     position: absolute;
 | 
				
			||||||
 | 
					     top: 0; left: 0;
 | 
				
			||||||
 | 
					     width: 100%;
 | 
				
			||||||
 | 
					     height: 100%;
 | 
				
			||||||
 | 
					     background: linear-gradient(180deg, var(--base-700) 0%, var(--base-700) 5%, rgba(var(--base-700-rgb), 0.0) 50%);
 | 
				
			||||||
 | 
					     filter: string.unquote("opacity(max(var(--overlay-opacity, 0), 0))");
 | 
				
			||||||
 | 
					     pointer-events: var(--should-display);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .servers {
 | 
				
			||||||
 | 
					     width: var(--sidebar-server-select-width);
 | 
				
			||||||
 | 
					     background-color: var(--base-500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     & > button {
 | 
				
			||||||
 | 
						 border: 0;
 | 
				
			||||||
 | 
						 padding: 0;
 | 
				
			||||||
 | 
						 margin:0;
 | 
				
			||||||
 | 
						 height: calc(var(--sidebar-server-select-width) - 16px);
 | 
				
			||||||
 | 
						 margin: 8px;
 | 
				
			||||||
 | 
						 border-radius: 12px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 background-color: var(--base-300);
 | 
				
			||||||
 | 
						 color: var(--base-700);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 transition-property: background-color, color, border-radius;
 | 
				
			||||||
 | 
						 transition-duration: 80ms;
 | 
				
			||||||
 | 
						 transition-timing-function: ease-out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 &:hover {
 | 
				
			||||||
 | 
						     background-color: var(--base-400);
 | 
				
			||||||
 | 
						 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 &.selected {
 | 
				
			||||||
 | 
						     background-color: var(--base-700);
 | 
				
			||||||
 | 
						     color: var(--base-300);
 | 
				
			||||||
 | 
						 }
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										162
									
								
								src/lib/SignIn.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								src/lib/SignIn.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					  import { decodeJwt } from 'jose';
 | 
				
			||||||
 | 
					  import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
					  import { fly, slide } from 'svelte/transition';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let token = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let selectedMethod = 'token';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function isValidJwt(token: string) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      decodeJwt(token);
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let error: boolean = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function signIn() {
 | 
				
			||||||
 | 
					    let detail = {};
 | 
				
			||||||
 | 
					    console.log(token);
 | 
				
			||||||
 | 
					    switch (selectedMethod) {
 | 
				
			||||||
 | 
					      case 'token':
 | 
				
			||||||
 | 
					        if (!token || !isValidJwt(token)) {
 | 
				
			||||||
 | 
					          error = true;
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        detail.type = 'token';
 | 
				
			||||||
 | 
					        detail.data = token;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dispatch('signIn', {
 | 
				
			||||||
 | 
					      method: selectedMethod,
 | 
				
			||||||
 | 
					      ...detail
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="d-flex col w-100 tab-bar">
 | 
				
			||||||
 | 
					  <div class="d-flex align-center signin-types">
 | 
				
			||||||
 | 
					    <p class="no-select">Method:</p>
 | 
				
			||||||
 | 
					    <div class="signin-buttons grow-1 d-flex">
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        class="grow-1"
 | 
				
			||||||
 | 
					        disabled
 | 
				
			||||||
 | 
					        on:click={() => (selectedMethod = 'userpass')}
 | 
				
			||||||
 | 
					        class:selected={selectedMethod == 'userpass'}>Username</button
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        class="grow-1"
 | 
				
			||||||
 | 
					        on:click={() => (selectedMethod = 'token')}
 | 
				
			||||||
 | 
					        class:selected={selectedMethod == 'token'}>Token</button
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="signin-field">
 | 
				
			||||||
 | 
					  {#if selectedMethod == 'token'}
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <textarea
 | 
				
			||||||
 | 
					        class:error
 | 
				
			||||||
 | 
					        spellcheck="false"
 | 
				
			||||||
 | 
					        on:change={() => (error = false)}
 | 
				
			||||||
 | 
					        placeholder="Paste token here"
 | 
				
			||||||
 | 
					        bind:value={token}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      {#if error}
 | 
				
			||||||
 | 
					        <div class="error" style="font-size: 12px; position:absolute;" transition:fly>
 | 
				
			||||||
 | 
					          Invalid token
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      {/if}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  {/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="d-flex w-100 space-between submit-buttons">
 | 
				
			||||||
 | 
					  <div class="spacer" />
 | 
				
			||||||
 | 
					  <button on:click={() => dispatch('close')} class="close">Close</button>
 | 
				
			||||||
 | 
					  <button on:click={signIn} class="signin">Sign In</button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
					  .selected {
 | 
				
			||||||
 | 
					    background-color: var(--base-600);
 | 
				
			||||||
 | 
					    color: var(--base-700);
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    margin-left: -32px;
 | 
				
			||||||
 | 
					    margin-right: -32px;
 | 
				
			||||||
 | 
					    border-radius: 24px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:first-child {
 | 
				
			||||||
 | 
					      margin-left: 0;
 | 
				
			||||||
 | 
					      border-top-left-radius: 4px;
 | 
				
			||||||
 | 
					      border-bottom-left-radius: 4px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:last-child {
 | 
				
			||||||
 | 
					      margin-right: 0;
 | 
				
			||||||
 | 
					      border-top-right-radius: 4px;
 | 
				
			||||||
 | 
					      border-bottom-right-radius: 4px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .space-between {
 | 
				
			||||||
 | 
					    & > :not(:last-child) {
 | 
				
			||||||
 | 
					      margin-right: 8px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .signin-field {
 | 
				
			||||||
 | 
					    margin-top: 8px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .tab-bar {
 | 
				
			||||||
 | 
					    & p {
 | 
				
			||||||
 | 
					      margin-top: 8px;
 | 
				
			||||||
 | 
					      margin-bottom: 8px;
 | 
				
			||||||
 | 
					      margin-right: 16px;
 | 
				
			||||||
 | 
					      font-style: oblique;
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  textarea {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    height: 150px;
 | 
				
			||||||
 | 
					    background-color: var(--base-800);
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					    word-break: break-all;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error {
 | 
				
			||||||
 | 
					    border-color: var(--red);
 | 
				
			||||||
 | 
					    outline-color: var(--red);
 | 
				
			||||||
 | 
					    color: var(--red);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .submit-buttons {
 | 
				
			||||||
 | 
					    margin-top: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .signin {
 | 
				
			||||||
 | 
					      background-color: var(--cyan);
 | 
				
			||||||
 | 
					      color: var(--base-700);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    & button {
 | 
				
			||||||
 | 
					      height: 36px;
 | 
				
			||||||
 | 
					      padding-left: 32px;
 | 
				
			||||||
 | 
					      padding-right: 32px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										316
									
								
								src/lib/TopScreen.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								src/lib/TopScreen.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,316 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import Header from './Header.svelte';
 | 
				
			||||||
 | 
					 import { globalMode, theme } from './settings';
 | 
				
			||||||
 | 
					 import { mentions } from './chat';
 | 
				
			||||||
 | 
					 import MessageView from './MessageView.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
 | 
				
			||||||
 | 
					 import { faEarthEurope, faPaintRoller, faChevronRight } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { Svrollbar } from 'svrollbar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { slide } from 'svelte/transition';
 | 
				
			||||||
 | 
					 import { tick, onMount } from 'svelte';
 | 
				
			||||||
 | 
					  import About from './About.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 export let clientHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let viewport: Element, contents: Element;
 | 
				
			||||||
 | 
					 let stickToBottom: boolean = false;
 | 
				
			||||||
 | 
					 function onScroll() {
 | 
				
			||||||
 | 
					     const scrollOffset = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     if (scrollOffset >= 80) {
 | 
				
			||||||
 | 
						 stickToBottom = false;
 | 
				
			||||||
 | 
					     } else if (scrollOffset < 80) {
 | 
				
			||||||
 | 
						 stickToBottom = true;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 onMount(() => {
 | 
				
			||||||
 | 
					     viewport &&
 | 
				
			||||||
 | 
					     viewport.scrollTo({
 | 
				
			||||||
 | 
					         top: viewport.scrollHeight,
 | 
				
			||||||
 | 
					     });
 | 
				
			||||||
 | 
					 })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 mentions.subscribe(async () => {
 | 
				
			||||||
 | 
					     if (stickToBottom) {
 | 
				
			||||||
 | 
						 // Wait for the message to be added to the DOM
 | 
				
			||||||
 | 
						 await tick();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 // Scroll the list to the bottom with the new DOM
 | 
				
			||||||
 | 
						 viewport &&
 | 
				
			||||||
 | 
						 viewport.scrollTo({
 | 
				
			||||||
 | 
					             top: viewport.scrollHeight,
 | 
				
			||||||
 | 
						 });
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function globalModeClick() {
 | 
				
			||||||
 | 
					     $globalMode = !$globalMode;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div transition:slide class="top screen" bind:clientHeight>
 | 
				
			||||||
 | 
					    <div class="about">
 | 
				
			||||||
 | 
						<About />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <Header flex --header-color="var(--top-screen-header-color)">
 | 
				
			||||||
 | 
						this is where instance branding goes
 | 
				
			||||||
 | 
					    </Header>
 | 
				
			||||||
 | 
					    <div class="contents">
 | 
				
			||||||
 | 
						<div class="settings">
 | 
				
			||||||
 | 
						    <div class="quick-settings">
 | 
				
			||||||
 | 
							<div class="button" on:click="{globalModeClick}"
 | 
				
			||||||
 | 
							     class:enabled="{$globalMode}">
 | 
				
			||||||
 | 
							    <div class="button-icon">
 | 
				
			||||||
 | 
								<FontAwesomeIcon icon="{faEarthEurope}" />
 | 
				
			||||||
 | 
							    </div>
 | 
				
			||||||
 | 
							    <div class="button-text">
 | 
				
			||||||
 | 
								<div class="title">
 | 
				
			||||||
 | 
								    Global Mode
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="subtitle">
 | 
				
			||||||
 | 
								    {$globalMode ? 'Enabled' : 'Disabled'}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							    </div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<div class="button">
 | 
				
			||||||
 | 
							    <div class="button-icon">
 | 
				
			||||||
 | 
								<FontAwesomeIcon icon="{faPaintRoller}" />
 | 
				
			||||||
 | 
							    </div>
 | 
				
			||||||
 | 
							    <div class="button-text">
 | 
				
			||||||
 | 
								<div class="title">
 | 
				
			||||||
 | 
								    Theme
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="subtitle">
 | 
				
			||||||
 | 
								    {$theme}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							    </div>
 | 
				
			||||||
 | 
							    <div class="button-icon">
 | 
				
			||||||
 | 
								<FontAwesomeIcon icon="{faChevronRight}" />
 | 
				
			||||||
 | 
							    </div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<div class="button" />
 | 
				
			||||||
 | 
							<div class="button" />
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						    <div class="spacer"/>
 | 
				
			||||||
 | 
						    <div class="options-bar">
 | 
				
			||||||
 | 
							<div class="button enabled">Edit</div>
 | 
				
			||||||
 | 
							<div class="button enabled">Theme</div>
 | 
				
			||||||
 | 
							<div class="button enabled">Settings</div>
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						    <div class="spacer"/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="recent-mentions">
 | 
				
			||||||
 | 
						    <div class="caption">Recent Mentions:</div>
 | 
				
			||||||
 | 
						    <div class="scroller">
 | 
				
			||||||
 | 
							{#if $mentions.length}
 | 
				
			||||||
 | 
							    <Svrollbar alwaysVisible {viewport} {contents}
 | 
				
			||||||
 | 
								       margin="{{top: 8, bottom: 8, right: 4}}"
 | 
				
			||||||
 | 
								       --svrollbar-track-width="8px" --svrollbar-thumb-width="4px" />
 | 
				
			||||||
 | 
							    <MessageView messages="{$mentions}" showChannel bind:viewport bind:contents
 | 
				
			||||||
 | 
									 on:scroll="{onScroll}" />
 | 
				
			||||||
 | 
							{:else}
 | 
				
			||||||
 | 
							    <div class="viewport" bind:this="{viewport}">
 | 
				
			||||||
 | 
								<div class="empty-mentions">
 | 
				
			||||||
 | 
								    <p class="caption">Nobody has mentioned you yet</p>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							    </div>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					 :global(body) {
 | 
				
			||||||
 | 
					     --top-screen-cutout-height: 20vh;
 | 
				
			||||||
 | 
					     --top-screen-background-color: var(--base-700);
 | 
				
			||||||
 | 
					     --top-screen-header-color: var(--base-800);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .top {
 | 
				
			||||||
 | 
					     height: calc(100vh - var(--top-screen-cutout-height));
 | 
				
			||||||
 | 
					     max-height: calc(100vh - var(--top-screen-cutout-height));
 | 
				
			||||||
 | 
					     background-color: var(--top-screen-background-color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     flex-direction: column;
 | 
				
			||||||
 | 
					     position: relative;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 :global(.container.mobile) .about {
 | 
				
			||||||
 | 
					     display: none;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .about {
 | 
				
			||||||
 | 
					     position: absolute;
 | 
				
			||||||
 | 
					     z-index: 500;
 | 
				
			||||||
 | 
					     bottom: calc(-1 * var(--top-screen-cutout-height));
 | 
				
			||||||
 | 
					     opacity: var(--about-opacity);
 | 
				
			||||||
 | 
					     display: var(--about-display);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 :global(.container.mobile) .settings {
 | 
				
			||||||
 | 
					     display: contents;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 :global(.container:not(.mobile)) .settings {
 | 
				
			||||||
 | 
					     flex-grow: 1;
 | 
				
			||||||
 | 
					     flex-basis: 0;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 :global(.container.mobile) .contents {
 | 
				
			||||||
 | 
					     flex-direction: column;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .contents {
 | 
				
			||||||
 | 
					     flex-grow: 1;
 | 
				
			||||||
 | 
					     position: relative;
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     padding-top: 14px;
 | 
				
			||||||
 | 
					     padding-left: 16px;
 | 
				
			||||||
 | 
					     padding-right: 16px;
 | 
				
			||||||
 | 
					     margin-bottom: 48px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .quick-settings {
 | 
				
			||||||
 | 
					     display: grid;
 | 
				
			||||||
 | 
					     grid-template-rows: repeat(2, 1fr);
 | 
				
			||||||
 | 
					     grid-template-columns: repeat(2, 50%);
 | 
				
			||||||
 | 
					     height: 35%;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .button {
 | 
				
			||||||
 | 
					     margin: 8px;
 | 
				
			||||||
 | 
					     border-radius: 32px;
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     align-items: center;
 | 
				
			||||||
 | 
					     padding: 8px;
 | 
				
			||||||
 | 
					     /* padding-left: 24px; */
 | 
				
			||||||
 | 
					     font-size: 1.4em;
 | 
				
			||||||
 | 
					     transition: background-color;
 | 
				
			||||||
 | 
					     transition-duration: 250ms;
 | 
				
			||||||
 | 
					     cursor: pointer;
 | 
				
			||||||
 | 
					     user-select: none;
 | 
				
			||||||
 | 
					     font-size: calc(15px + 0.390625vw);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .button.enabled {
 | 
				
			||||||
 | 
					     background: var(--base-600);
 | 
				
			||||||
 | 
					     color: var(--base-700);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .button:not(.enabled) {
 | 
				
			||||||
 | 
					     background: var(--base-800);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .button .button-icon {
 | 
				
			||||||
 | 
					     padding-left: 8px;
 | 
				
			||||||
 | 
					     padding-right: 8px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .button-text {
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     flex-direction: column;
 | 
				
			||||||
 | 
					     min-width: 0;
 | 
				
			||||||
 | 
					     flex-grow: 1;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .button .title {
 | 
				
			||||||
 | 
					     font-weight: bold;
 | 
				
			||||||
 | 
					     font-size: .9em;
 | 
				
			||||||
 | 
					     margin-bottom: 2px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .button .subtitle {
 | 
				
			||||||
 | 
					     font-size: .85em;
 | 
				
			||||||
 | 
					     font-weight: 300;
 | 
				
			||||||
 | 
					     text-overflow: ellipsis;
 | 
				
			||||||
 | 
					     white-space: nowrap;
 | 
				
			||||||
 | 
					     overflow: hidden;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .options-bar {
 | 
				
			||||||
 | 
					     display: grid;
 | 
				
			||||||
 | 
					     grid-template-columns: repeat(3, 1fr);
 | 
				
			||||||
 | 
					     height: 10%;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .options-bar .button {
 | 
				
			||||||
 | 
					     margin-top: 8px;
 | 
				
			||||||
 | 
					     margin-bottom: 8px;
 | 
				
			||||||
 | 
					     border-radius: 12px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .options-bar .button:nth-child(2) {
 | 
				
			||||||
 | 
					     margin-left: 4px;
 | 
				
			||||||
 | 
					     margin-right: 4px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .spacer {
 | 
				
			||||||
 | 
					     flex-grow: 1;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .scroller {
 | 
				
			||||||
 | 
					     min-height: 0;
 | 
				
			||||||
 | 
					     flex-grow: 1;
 | 
				
			||||||
 | 
					     flex-basis: 0;
 | 
				
			||||||
 | 
					     position: relative;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .scroller > :global(.viewport) {
 | 
				
			||||||
 | 
					     overflow: scroll;
 | 
				
			||||||
 | 
					     height: 100% !important;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .scroller > :global(*) {
 | 
				
			||||||
 | 
					     /* hide scrollbar */
 | 
				
			||||||
 | 
					     -ms-overflow-style: none !important;
 | 
				
			||||||
 | 
					     scrollbar-width: none !important;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .scroller > :global(*::-webkit-scrollbar) {
 | 
				
			||||||
 | 
					     /* hide scrollbar */
 | 
				
			||||||
 | 
					     display: none !important;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .scroller :global(.v-scrollbar) {
 | 
				
			||||||
 | 
					     z-index: 102;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 :global(.container.mobile) .recent-mentions {
 | 
				
			||||||
 | 
					     height: 50%;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 :global(.container:not(.mobile)) .recent-mentions {
 | 
				
			||||||
 | 
					     flex-grow: 1;
 | 
				
			||||||
 | 
					     flex-basis: 0;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .recent-mentions {
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .caption {
 | 
				
			||||||
 | 
					     font-size: 12px;
 | 
				
			||||||
 | 
					     line-height: 2;
 | 
				
			||||||
 | 
					     padding-left: 8px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .recent-mentions :global(.viewport) {
 | 
				
			||||||
 | 
					     background-color: var(--base-800);
 | 
				
			||||||
 | 
					     border-radius: 12px;
 | 
				
			||||||
 | 
					     /* border: 1.6px solid black; */
 | 
				
			||||||
 | 
					     box-shadow: rgb(0 0 0 / 18%) 0px 2px 4px 0px inset;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					 .recent-mentions :global(.message) {
 | 
				
			||||||
 | 
					     margin-left: 8px;
 | 
				
			||||||
 | 
					     margin-right: 8px;
 | 
				
			||||||
 | 
					     border-radius: 12px;
 | 
				
			||||||
 | 
					     padding: 8px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .empty-mentions {
 | 
				
			||||||
 | 
					     height: 100%;
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     flex-direction: column;
 | 
				
			||||||
 | 
					     align-items: center;
 | 
				
			||||||
 | 
					     justify-content: center;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										139
									
								
								src/lib/UserSettings.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/lib/UserSettings.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import Avatar from "./Avatar.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { color, username, num, isTroll } from "./chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
 | 
				
			||||||
 | 
					 import { faHashtag, faRightFromBracket, faRightToBracket, faRotateRight } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function signIn() {
 | 
				
			||||||
 | 
					     dispatch('signIn')
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function signOut() {
 | 
				
			||||||
 | 
					     dispatch('signOut')
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="d-flex col user-settings no-select">
 | 
				
			||||||
 | 
					    <div class="d-flex">
 | 
				
			||||||
 | 
						<Avatar color="{$color}" username="{$username}" />
 | 
				
			||||||
 | 
						<div class="d-flex col username">
 | 
				
			||||||
 | 
						    <div>
 | 
				
			||||||
 | 
							{$username}
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						    <div class="d-flex">
 | 
				
			||||||
 | 
							<FontAwesomeIcon size="sm" icon="{faHashtag}"  />
 | 
				
			||||||
 | 
							<div>
 | 
				
			||||||
 | 
							    {$num}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <hr>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {#if $isTroll}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div on:click={signIn}>
 | 
				
			||||||
 | 
						    <div>
 | 
				
			||||||
 | 
							<FontAwesomeIcon icon="{faRightToBracket}" />
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						    <div>
 | 
				
			||||||
 | 
							Sign In
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div on:click={signOut}>
 | 
				
			||||||
 | 
						    <div>
 | 
				
			||||||
 | 
							<FontAwesomeIcon icon="{faRotateRight}" />
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						    <div>
 | 
				
			||||||
 | 
							Refresh Troll Token
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:else}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div on:click={signOut}>
 | 
				
			||||||
 | 
						    <div>
 | 
				
			||||||
 | 
							<FontAwesomeIcon icon="{faRightFromBracket}" />
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						    <div>
 | 
				
			||||||
 | 
							Sign Out
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
					 @use "$lib/styles/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .user-settings {
 | 
				
			||||||
 | 
					     width: 250px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     margin-left: 16px;
 | 
				
			||||||
 | 
					     margin-right: 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     padding-top: 16px;
 | 
				
			||||||
 | 
					     padding-bottom: 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     background: var(--base-700);
 | 
				
			||||||
 | 
					     color: var(--base-300);
 | 
				
			||||||
 | 
					     border-radius: 12px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .username {
 | 
				
			||||||
 | 
					     & > :first-child {
 | 
				
			||||||
 | 
						 font-weight: bold;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     & > :last-child {
 | 
				
			||||||
 | 
						 font-weight: 400;
 | 
				
			||||||
 | 
						 font-size: 90%;
 | 
				
			||||||
 | 
						 align-items: center;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 hr {
 | 
				
			||||||
 | 
					     padding: 0 !important;
 | 
				
			||||||
 | 
					     width: 100%;
 | 
				
			||||||
 | 
					     border-color: var(--base-600);
 | 
				
			||||||
 | 
					     border-width: 0;
 | 
				
			||||||
 | 
					     border-top-width: 0.5px;
 | 
				
			||||||
 | 
					     border-bottom-width: 0.5px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .user-settings {
 | 
				
			||||||
 | 
					     border: 1px solid;
 | 
				
			||||||
 | 
					     box-shadow: rgb(0 0 0 / 12%) 0px 1px 3px, rgb(0 0 0 / 24%) 0px 1px 2px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     & > :first-child {
 | 
				
			||||||
 | 
						 padding-left: 8px;
 | 
				
			||||||
 | 
						 padding-right: 8px;
 | 
				
			||||||
 | 
						 padding-top: 0;
 | 
				
			||||||
 | 
						 padding-right: 0;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     & > * {
 | 
				
			||||||
 | 
						 @extend .d-flex;
 | 
				
			||||||
 | 
						 padding: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 & > :global(:first-child) {
 | 
				
			||||||
 | 
						     margin-right: 16px;
 | 
				
			||||||
 | 
						     margin-left: 8px;
 | 
				
			||||||
 | 
						 }
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     & > :not(:first-child):hover {
 | 
				
			||||||
 | 
						 cursor: pointer;
 | 
				
			||||||
 | 
						 background-color: var(--base-800);
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										172
									
								
								src/lib/chat.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/lib/chat.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,172 @@
 | 
				
			|||||||
 | 
					import { writable, readable, derived, get, type Subscriber } from 'svelte/store';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  FedwaveChat,
 | 
				
			||||||
 | 
					  type Message,
 | 
				
			||||||
 | 
					  type OutgoingMessage,
 | 
				
			||||||
 | 
					  type Credentials
 | 
				
			||||||
 | 
					} from 'fedwave-chat-client';
 | 
				
			||||||
 | 
					import { globalMode } from './settings';
 | 
				
			||||||
 | 
					import * as jose from 'jose';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const chat = new FedwaveChat();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type TaggedMessage = {
 | 
				
			||||||
 | 
					  m: Message;
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  isHeader: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function send(m: string | OutgoingMessage) {
 | 
				
			||||||
 | 
					  chat.sendMessage(m);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let setConnected: Subscriber<boolean> | undefined;
 | 
				
			||||||
 | 
					export const connected = readable<boolean>(false, (setf) => {
 | 
				
			||||||
 | 
					  setConnected = setf;
 | 
				
			||||||
 | 
					  return () => {
 | 
				
			||||||
 | 
					    setConnected = () => {};
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getCredentialsFromLocalStorage() {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const token: Credentials | null = JSON.parse(localStorage.getItem('chatToken') || '""') || null;
 | 
				
			||||||
 | 
					    if (token !== null) {
 | 
				
			||||||
 | 
					      return token;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error(e);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let setCredentials: Subscriber<Credentials> | undefined;
 | 
				
			||||||
 | 
					export const credentials = readable<Credentials>(getCredentialsFromLocalStorage(), (setf) => {
 | 
				
			||||||
 | 
					  setCredentials = setf;
 | 
				
			||||||
 | 
					  return () => {
 | 
				
			||||||
 | 
					    setCredentials = () => {};
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const updateCredentials = (creds: Credentials) => {
 | 
				
			||||||
 | 
					  setCredentials?.(creds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (get(connected)) {
 | 
				
			||||||
 | 
					    chat.disconnect();
 | 
				
			||||||
 | 
					    chat.connect(get(room), creds);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					chat.onUpdateCredentials = async function (creds) {
 | 
				
			||||||
 | 
					  setCredentials?.(creds);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const decodedToken = derived(credentials, (creds) => {
 | 
				
			||||||
 | 
					  const token = typeof creds == 'string' ? creds : creds?.jwt || '';
 | 
				
			||||||
 | 
					  let r;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    r = jose.decodeJwt(token).sub as any;
 | 
				
			||||||
 | 
					  } catch {}
 | 
				
			||||||
 | 
					  return r;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const username = derived(decodedToken, (token) => {
 | 
				
			||||||
 | 
					  return token?.username;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const trollRegex = /^troll:[\w]{4}$/i;
 | 
				
			||||||
 | 
					export const isTroll = derived(username, (usr) => {
 | 
				
			||||||
 | 
					  return trollRegex.test(usr);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const color = derived(decodedToken, (token) => {
 | 
				
			||||||
 | 
					  return token?.color;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const num = derived(decodedToken, (token) => {
 | 
				
			||||||
 | 
					  return token?.num;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const room = writable<string>(chat.room);
 | 
				
			||||||
 | 
					room.subscribe((room) => {
 | 
				
			||||||
 | 
					  chat.room = room;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					globalMode.subscribe((v) => {
 | 
				
			||||||
 | 
					  chat.global = v;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const shouldTweet = writable(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Essentially a shared_ptr idiom. When nobody is subscribed to this, the chat
 | 
				
			||||||
 | 
					// d/c's.
 | 
				
			||||||
 | 
					export const chat_lock = readable(null, () => {
 | 
				
			||||||
 | 
					  const release_connected = connected.subscribe(() => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const creds = get(credentials);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chat.connect(chat.room, creds).then(() => setConnected?.(true));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return () => {
 | 
				
			||||||
 | 
					    chat.disconnect();
 | 
				
			||||||
 | 
					    setConnected?.(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    release_connected();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const messageLimit = writable<number>(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const usernameRegex = derived(username, (user) => {
 | 
				
			||||||
 | 
					  return new RegExp(`(@${user})\\b`, 'gi');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const mentions = writable<TaggedMessage[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const messages = readable<TaggedMessage[]>([], (set) => {
 | 
				
			||||||
 | 
					  chat.rcvMessageBulk = function (ms) {
 | 
				
			||||||
 | 
					    const usrRegex = get(usernameRegex);
 | 
				
			||||||
 | 
					    const old = get(messages);
 | 
				
			||||||
 | 
					    let prev = old[old.length - 1]?.m ?? { username: null, timestamp: 0 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let containsUsername = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 0; i < ms.length; ++i) {
 | 
				
			||||||
 | 
					      ms[i].message = ms[i].message.replaceAll(usrRegex, `<span class="mention">$1</span>`);
 | 
				
			||||||
 | 
					      const tagged: TaggedMessage = {
 | 
				
			||||||
 | 
					        m: ms[i],
 | 
				
			||||||
 | 
					        id: ms[i].username + ms[i].timestamp,
 | 
				
			||||||
 | 
					        isHeader:
 | 
				
			||||||
 | 
					          prev.username != ms[i].username || Math.abs(prev.timestamp - ms[i].timestamp) > 60000
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (usrRegex.test(ms[i].message)) {
 | 
				
			||||||
 | 
					        mentions.update((m) => m.concat({ ...tagged, isHeader: true }));
 | 
				
			||||||
 | 
					        containsUsername = true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      old.push(tagged);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      prev = ms[i];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const limit = get(messageLimit);
 | 
				
			||||||
 | 
					    if (old.length > limit) {
 | 
				
			||||||
 | 
					      old.splice(0, old.length - limit);
 | 
				
			||||||
 | 
					      old[0].isHeader = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    shouldTweet.set(containsUsername);
 | 
				
			||||||
 | 
					    set(old);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return () => {
 | 
				
			||||||
 | 
					    chat.rcvMessageBulk = function () {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const localMessages = derived([messages, room, globalMode], ([ms, room, global]) => {
 | 
				
			||||||
 | 
					  if (global) return ms;
 | 
				
			||||||
 | 
					  return ms.filter((m) => m.m.channel.toLowerCase() === room.toLowerCase());
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/lib/settings/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/lib/settings/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					import { writable } from 'svelte/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const globalMode = writable(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Theme = 'solarized-light' | 'solarized-dark';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const theme = writable<Theme>('solarized-light');
 | 
				
			||||||
							
								
								
									
										40
									
								
								src/lib/styles/global.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/lib/styles/global.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					body {
 | 
				
			||||||
 | 
					    background-color: var(--base-800);
 | 
				
			||||||
 | 
					    color: var(--base-300);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a {
 | 
				
			||||||
 | 
					    color: var(--blue);
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					    &:visited {
 | 
				
			||||||
 | 
						color: var(--violet);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button {
 | 
				
			||||||
 | 
					    font-family: inherit;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    border: 1.4px solid var(--base-600);
 | 
				
			||||||
 | 
					    background-color: var(--base-700);
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					    height: 24px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    transition: background-color 80ms ease-in-out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
						background-color: var(--base-800);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:disabled {
 | 
				
			||||||
 | 
						border: 1.4px dashed var(--base-600);
 | 
				
			||||||
 | 
						cursor: not-allowed;
 | 
				
			||||||
 | 
						background-color: var(--base-700);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:active:not(:disabled) {
 | 
				
			||||||
 | 
						border-color: var(--base-300);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outline-color: var(--base-300);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										47
									
								
								src/lib/styles/solarized.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/lib/styles/solarized.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					@use "$lib/styles/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$solarized-light: (
 | 
				
			||||||
 | 
					    "base-100": #002b36,
 | 
				
			||||||
 | 
					    "base-200": #073642,
 | 
				
			||||||
 | 
					    "base-300": #586e75,
 | 
				
			||||||
 | 
					    "base-400": #657b83,
 | 
				
			||||||
 | 
					    "base-500": #839496,
 | 
				
			||||||
 | 
					    "base-600": #93a1a1,
 | 
				
			||||||
 | 
					    "base-700": #eee8d5,
 | 
				
			||||||
 | 
					    "base-800": #fdf6e3,
 | 
				
			||||||
 | 
					    "yellow": #b58900,
 | 
				
			||||||
 | 
					    "orange": #cb4b16,
 | 
				
			||||||
 | 
					    "red": #dc322f,
 | 
				
			||||||
 | 
					    "magenta": #d33682,
 | 
				
			||||||
 | 
					    "violet": #6c71c4,
 | 
				
			||||||
 | 
					    "blue": #268bd2,
 | 
				
			||||||
 | 
					    "cyan": #2aa198,
 | 
				
			||||||
 | 
					    "green": #859900,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$solarized-dark: (
 | 
				
			||||||
 | 
					    "base-800": #002b36,
 | 
				
			||||||
 | 
					    "base-700": #073642,
 | 
				
			||||||
 | 
					    "base-600": #586e75,
 | 
				
			||||||
 | 
					    "base-500": #657b83,
 | 
				
			||||||
 | 
					    "base-400": #839496,
 | 
				
			||||||
 | 
					    "base-300": #93a1a1,
 | 
				
			||||||
 | 
					    "base-200": #eee8d5,
 | 
				
			||||||
 | 
					    "base-100": #fdf6e3,
 | 
				
			||||||
 | 
					    "yellow": #b58900,
 | 
				
			||||||
 | 
					    "orange": #cb4b16,
 | 
				
			||||||
 | 
					    "red": #dc322f,
 | 
				
			||||||
 | 
					    "magenta": #d33682,
 | 
				
			||||||
 | 
					    "violet": #6c71c4,
 | 
				
			||||||
 | 
					    "blue": #268bd2,
 | 
				
			||||||
 | 
					    "cyan": #2aa198,
 | 
				
			||||||
 | 
					    "green": #859900,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.solarized-light {
 | 
				
			||||||
 | 
					    @include utils.df-scheme($solarized-light);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.solarized-dark {
 | 
				
			||||||
 | 
					    @include utils.df-scheme($solarized-dark);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										91
									
								
								src/lib/styles/utils.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/lib/styles/utils.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
				
			|||||||
 | 
					@use "sass:color";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mixin df-c($name, $color) {
 | 
				
			||||||
 | 
					    --#{$name}: #{$color};
 | 
				
			||||||
 | 
					    --#{$name}-rgb: #{color.red($color)}, #{color.green($color)}, #{color.blue($color)};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mixin df-scheme($scheme) {
 | 
				
			||||||
 | 
					    @each $name, $color in $scheme {
 | 
				
			||||||
 | 
						@include df-c($name, $color);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.no-select {
 | 
				
			||||||
 | 
					    user-select: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.clickable {
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.spacer, .grow-1 {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.grow-0 {
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.basis-0 {
 | 
				
			||||||
 | 
					    flex-basis: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.snap-point {
 | 
				
			||||||
 | 
					    scroll-snap-align: start;
 | 
				
			||||||
 | 
					    scroll-snap-stop: always;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.snap-x {
 | 
				
			||||||
 | 
					    overflow: scroll;
 | 
				
			||||||
 | 
					    scroll-snap-type: x mandatory;
 | 
				
			||||||
 | 
					    & > * {
 | 
				
			||||||
 | 
						@extend .snap-point;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.snap-y {
 | 
				
			||||||
 | 
					    overflow: scroll;
 | 
				
			||||||
 | 
					    scroll-snap-type: y mandatory;
 | 
				
			||||||
 | 
					    & > * {
 | 
				
			||||||
 | 
						@extend .snap-point;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.d-flex {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.col {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.align-center {
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.h-100vh {
 | 
				
			||||||
 | 
					    height: 100vh;
 | 
				
			||||||
 | 
					    max-height: 100vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.w-100vw {
 | 
				
			||||||
 | 
					    width: 100vw;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.w-100 { width: 100%; }
 | 
				
			||||||
 | 
					.h-100 { height: 100%; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hide-scrollbar {
 | 
				
			||||||
 | 
					    /* hide scrollbar */
 | 
				
			||||||
 | 
					    -ms-overflow-style: none !important;
 | 
				
			||||||
 | 
					    scrollbar-width: none !important;
 | 
				
			||||||
 | 
					    &::-webkit-scrollbar {
 | 
				
			||||||
 | 
						display: none !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.divider {
 | 
				
			||||||
 | 
					    border: .5px solid var(--base-600);
 | 
				
			||||||
 | 
					    margin-top: 2px;
 | 
				
			||||||
 | 
					    margin-bottom: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								src/lib/ui/FAB.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/lib/ui/FAB.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<button class="fab" on:click>
 | 
				
			||||||
 | 
					    <slot />
 | 
				
			||||||
 | 
					</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					 .fab {
 | 
				
			||||||
 | 
					     position: absolute;
 | 
				
			||||||
 | 
					     left: var(--left, calc(100% - 2 * var(--size, 48px)));
 | 
				
			||||||
 | 
					     top: var(--top, calc(100% - 2 * var(--size, 48px)));
 | 
				
			||||||
 | 
					     border-radius: 9999px;
 | 
				
			||||||
 | 
					     width: var(--size, 48px);
 | 
				
			||||||
 | 
					     height: var(--size, 48px);
 | 
				
			||||||
 | 
					     background: var(--background, white);
 | 
				
			||||||
 | 
					     border: none;
 | 
				
			||||||
 | 
					     z-index: 200;
 | 
				
			||||||
 | 
					     box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
 | 
				
			||||||
 | 
					     cursor: pointer;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .fab > * { z-index: 202; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .fab:active::before {
 | 
				
			||||||
 | 
					     content: "";
 | 
				
			||||||
 | 
					     position: absolute;
 | 
				
			||||||
 | 
					     display: block;
 | 
				
			||||||
 | 
					     width: 100%;
 | 
				
			||||||
 | 
					     height: 100%;
 | 
				
			||||||
 | 
					     z-index: 201;
 | 
				
			||||||
 | 
					     background: rgba(0,0,0, 0.1);
 | 
				
			||||||
 | 
					     left: 0;
 | 
				
			||||||
 | 
					     top: 0;
 | 
				
			||||||
 | 
					     border-radius: 9999px;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										78
									
								
								src/lib/ui/Modal.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/lib/ui/Modal.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
					 import { createEventDispatcher, onDestroy } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					 const close = () => dispatch('close');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let modal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 const handle_keydown = e => {
 | 
				
			||||||
 | 
					     if (e.key === 'Escape') {
 | 
				
			||||||
 | 
						 close();
 | 
				
			||||||
 | 
						 return;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     if (e.key === 'Tab') {
 | 
				
			||||||
 | 
						 // trap focus
 | 
				
			||||||
 | 
						 const nodes = modal.querySelectorAll('*');
 | 
				
			||||||
 | 
						 const tabbable = Array.from(nodes).filter(n => n.tabIndex >= 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 let index = tabbable.indexOf(document.activeElement);
 | 
				
			||||||
 | 
						 if (index === -1 && e.shiftKey) index = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 index += tabbable.length + (e.shiftKey ? -1 : 1);
 | 
				
			||||||
 | 
						 index %= tabbable.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						 tabbable[index].focus();
 | 
				
			||||||
 | 
						 e.preventDefault();
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 const previously_focused = typeof document !== 'undefined' && document.activeElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 if (previously_focused) {
 | 
				
			||||||
 | 
					     onDestroy(() => {
 | 
				
			||||||
 | 
						 previously_focused.focus();
 | 
				
			||||||
 | 
					     });
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:window on:keydown={handle_keydown}/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="modal-background" on:click={close}></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="modal" role="dialog" aria-modal="true" bind:this={modal}>
 | 
				
			||||||
 | 
					    <slot name="header"></slot>
 | 
				
			||||||
 | 
					    <slot></slot>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					 .modal-background {
 | 
				
			||||||
 | 
					     position: fixed;
 | 
				
			||||||
 | 
					     top: 0;
 | 
				
			||||||
 | 
					     left: 0;
 | 
				
			||||||
 | 
					     width: 100%;
 | 
				
			||||||
 | 
					     height: 100%;
 | 
				
			||||||
 | 
					     background: rgba(0,0,0,0.3);
 | 
				
			||||||
 | 
					     z-index: 300;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .modal {
 | 
				
			||||||
 | 
					     position: absolute;
 | 
				
			||||||
 | 
					     left: 50%;
 | 
				
			||||||
 | 
					     top: 50%;
 | 
				
			||||||
 | 
					     width: calc(100vw - 4em);
 | 
				
			||||||
 | 
					     max-width: 32em;
 | 
				
			||||||
 | 
					     max-height: calc(100vh - 4em);
 | 
				
			||||||
 | 
					     overflow: auto;
 | 
				
			||||||
 | 
					     transform: translate(-50%,-50%);
 | 
				
			||||||
 | 
					     padding: 1em;
 | 
				
			||||||
 | 
					     border-radius: 12px;
 | 
				
			||||||
 | 
					     background: var(--base-700);
 | 
				
			||||||
 | 
					     z-index: 300;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 button {
 | 
				
			||||||
 | 
					     display: block;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										16
									
								
								src/lib/ui/clickOutside.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/lib/ui/clickOutside.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					export function clickOutside(node: Node) {
 | 
				
			||||||
 | 
					  const handleClick = (event: Event) => {
 | 
				
			||||||
 | 
					    if (!node.contains(event.target as Node)) {
 | 
				
			||||||
 | 
					      node.dispatchEvent(new CustomEvent('outclick'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  document.addEventListener('click', handleClick, true);
 | 
				
			||||||
 | 
					  document.addEventListener('touchmove', handleClick, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    destroy() {
 | 
				
			||||||
 | 
					      document.removeEventListener('click', handleClick, true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										297
									
								
								src/routes/+layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								src/routes/+layout.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,297 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import Header from "$lib/Header.svelte";
 | 
				
			||||||
 | 
					 import TopScreen from '$lib/TopScreen.svelte';
 | 
				
			||||||
 | 
					 import Avatar from '$lib/Avatar.svelte';
 | 
				
			||||||
 | 
					 import Sidebar from "$lib/Sidebar.svelte";
 | 
				
			||||||
 | 
					 import Modal from "$lib/ui/Modal.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
 | 
				
			||||||
 | 
					 import { faBars, faHashtag } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					 import { config } from '@fortawesome/fontawesome-svg-core';
 | 
				
			||||||
 | 
					 import '@fortawesome/fontawesome-svg-core/styles.css' // Import the CSS
 | 
				
			||||||
 | 
					 config.autoAddCss = false // Tell Font Awesome to skip adding the CSS automatically since it's being imported above
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import "$lib/styles/solarized.scss";
 | 
				
			||||||
 | 
					 import "$lib/styles/global.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { color, room, username, updateCredentials } from "$lib/chat";
 | 
				
			||||||
 | 
					 import { primaryInput } from 'detect-it';
 | 
				
			||||||
 | 
					 import { onMount } from "svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { clickOutside } from "$lib/ui/clickOutside";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { offset, flip, shift } from "@floating-ui/dom";
 | 
				
			||||||
 | 
					 import { createFloatingActions } from "svelte-floating-ui";
 | 
				
			||||||
 | 
					 import UserSettings from "$lib/UserSettings.svelte";
 | 
				
			||||||
 | 
					 import SignIn from "$lib/SignIn.svelte";
 | 
				
			||||||
 | 
					 const [ floatingRef, floatingContent ] = createFloatingActions({
 | 
				
			||||||
 | 
					     strategy: "absolute",
 | 
				
			||||||
 | 
					     placement: "bottom-end",
 | 
				
			||||||
 | 
					     middleware: [
 | 
				
			||||||
 | 
						 offset(6),
 | 
				
			||||||
 | 
						 flip({
 | 
				
			||||||
 | 
						     flipAlignment: false,
 | 
				
			||||||
 | 
						 }),
 | 
				
			||||||
 | 
						 shift(),
 | 
				
			||||||
 | 
					     ]
 | 
				
			||||||
 | 
					 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let showUserSettings = false;
 | 
				
			||||||
 | 
					 function closeUserSettings() {
 | 
				
			||||||
 | 
					     if(showUserSettings)
 | 
				
			||||||
 | 
						 showUserSettings = false;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let showSignIn = false;
 | 
				
			||||||
 | 
					 function signIn(e: CustomEvent<any>) {
 | 
				
			||||||
 | 
					     switch(e.detail.type) {
 | 
				
			||||||
 | 
						 case 'token':
 | 
				
			||||||
 | 
						     updateCredentials(e.detail.data);
 | 
				
			||||||
 | 
						     break;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     showSignIn = false;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function signOut() {
 | 
				
			||||||
 | 
					     closeUserSettings();
 | 
				
			||||||
 | 
					     updateCredentials(undefined);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let mobile = primaryInput === 'touch';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let scrolledToTop = false, scrolledToBottom = false,
 | 
				
			||||||
 | 
					     scrolledToLeft = false, scrolledToRight = false;
 | 
				
			||||||
 | 
					 let topHeight = 0;
 | 
				
			||||||
 | 
					 let sidebarWidth = 0;
 | 
				
			||||||
 | 
					 let container: Element, bottomContainer: Element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 let darkenAmount = 0;
 | 
				
			||||||
 | 
					 let sidebarDarkenAmount = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 onMount(() => {
 | 
				
			||||||
 | 
					     container.scrollTop = topHeight;
 | 
				
			||||||
 | 
					     bottomContainer.scrollLeft = sidebarWidth;
 | 
				
			||||||
 | 
					     scrolledToBottom = true;
 | 
				
			||||||
 | 
					     scrolledToRight = true;
 | 
				
			||||||
 | 
					 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function onScroll() {
 | 
				
			||||||
 | 
					     if(!scrolledToRight) {
 | 
				
			||||||
 | 
						 return false;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     darkenAmount = (topHeight - container.scrollTop) / topHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     if(container.scrollTop == 0) {
 | 
				
			||||||
 | 
						 scrolledToTop = true;
 | 
				
			||||||
 | 
					     } else {
 | 
				
			||||||
 | 
						 scrolledToTop = false;
 | 
				
			||||||
 | 
						 scrolledToBottom = false;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     if(topHeight - container.scrollTop <= 1) {
 | 
				
			||||||
 | 
						 scrolledToBottom = true;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function onHorizontalScroll() {
 | 
				
			||||||
 | 
					     sidebarDarkenAmount = (sidebarWidth - bottomContainer.scrollLeft) / sidebarWidth;
 | 
				
			||||||
 | 
					     if(bottomContainer.scrollLeft == 0) {
 | 
				
			||||||
 | 
						 scrolledToLeft = true;
 | 
				
			||||||
 | 
					     } else {
 | 
				
			||||||
 | 
					 	 scrolledToLeft = false;
 | 
				
			||||||
 | 
						 scrolledToRight = false;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     if(sidebarWidth - bottomContainer.scrollLeft <= 1) {
 | 
				
			||||||
 | 
						 scrolledToRight = true;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function overlayClick() {
 | 
				
			||||||
 | 
					     if(scrolledToTop) {
 | 
				
			||||||
 | 
						 container.scrollTo({top: topHeight, behavior: 'smooth'});
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function sidebarOverlayClick() {
 | 
				
			||||||
 | 
					     if(scrolledToLeft) {
 | 
				
			||||||
 | 
						 bottomContainer.scrollTo({left: sidebarWidth, behavior: 'smooth'});
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function spacerClick() {
 | 
				
			||||||
 | 
					     if(container && scrolledToRight) {
 | 
				
			||||||
 | 
						 if(scrolledToTop) {
 | 
				
			||||||
 | 
						     container.scrollTo({top: topHeight, behavior: 'smooth'});
 | 
				
			||||||
 | 
						 } else if(scrolledToBottom) {
 | 
				
			||||||
 | 
						     container.scrollTo({top: 0, behavior: 'smooth'});
 | 
				
			||||||
 | 
						 }
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 function hamburgerClick() {
 | 
				
			||||||
 | 
					     if(bottomContainer && scrolledToBottom) {
 | 
				
			||||||
 | 
						 if(scrolledToLeft) {
 | 
				
			||||||
 | 
						     bottomContainer.scrollTo({left: sidebarWidth, behavior: 'smooth'});
 | 
				
			||||||
 | 
						 } else if(scrolledToRight) {
 | 
				
			||||||
 | 
						     bottomContainer.scrollTo({left: 0, behavior: 'smooth'});
 | 
				
			||||||
 | 
						 }
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="container hide-scrollbar snap-y h-100vh"
 | 
				
			||||||
 | 
					     class:mobile="{mobile}"
 | 
				
			||||||
 | 
					     bind:this="{container}" on:scroll="{onScroll}"
 | 
				
			||||||
 | 
					     style:--overflow-y="{!scrolledToRight ? 'hidden' : 'scroll'}"
 | 
				
			||||||
 | 
					     style:--about-opacity="{darkenAmount}"
 | 
				
			||||||
 | 
					     style:--about-display="{scrolledToBottom ? 'none' : 'unset'}">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <TopScreen bind:clientHeight={topHeight} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="screen hide-scrollbar" on:scroll="{onHorizontalScroll}" bind:this="{bottomContainer}"
 | 
				
			||||||
 | 
					         style:--overflow-x="{!scrolledToBottom ? 'hidden' : 'scroll'}">
 | 
				
			||||||
 | 
						<Sidebar {mobile} bind:clientWidth="{sidebarWidth}"
 | 
				
			||||||
 | 
							 --overlay-opacity="{darkenAmount}"
 | 
				
			||||||
 | 
							 --should-display="{scrolledToBottom ? 'none' : 'unset'}" />
 | 
				
			||||||
 | 
						<div class="chat h-100vh d-flex col">
 | 
				
			||||||
 | 
						    <div class="overlay clickable"
 | 
				
			||||||
 | 
							 style:--opacity="{darkenAmount}"
 | 
				
			||||||
 | 
							 style:--should-display="{scrolledToBottom ? 'none' : 'unset'}"
 | 
				
			||||||
 | 
							 on:click="{overlayClick}" />
 | 
				
			||||||
 | 
						    {#if mobile}
 | 
				
			||||||
 | 
							<div class="sidebar-overlay clickable"
 | 
				
			||||||
 | 
							     style:--opacity="{sidebarDarkenAmount}"
 | 
				
			||||||
 | 
							     style:--should-display="{scrolledToRight ? 'none' : 'unset'}"
 | 
				
			||||||
 | 
							     on:click="{sidebarOverlayClick}" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						    {/if}
 | 
				
			||||||
 | 
						    <Header flex --header-color="var(--top-screen-background-color)">
 | 
				
			||||||
 | 
							<div class="header">
 | 
				
			||||||
 | 
							    {#if mobile}
 | 
				
			||||||
 | 
								<div class="vert-center hamburger clickable" on:click={hamburgerClick}>
 | 
				
			||||||
 | 
								    <FontAwesomeIcon size="lg" icon="{faBars}" />
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							    {/if}
 | 
				
			||||||
 | 
							    <div class="vert-center room-name no-select">
 | 
				
			||||||
 | 
								<FontAwesomeIcon style="padding-right: 4px;" icon="{faHashtag}" />{$room}
 | 
				
			||||||
 | 
							    </div>
 | 
				
			||||||
 | 
							    <div class="spacer clickable" on:click="{spacerClick}" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							    <div class="vert-center avatar clickable" use:floatingRef>
 | 
				
			||||||
 | 
								<Avatar on:click="{() => {showUserSettings = !showUserSettings}}"
 | 
				
			||||||
 | 
									color="{$color}" username="{$username}" />
 | 
				
			||||||
 | 
							    </div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						    </Header>
 | 
				
			||||||
 | 
						    <slot />
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {#if showUserSettings}
 | 
				
			||||||
 | 
						<div class="user-settings" style="position:absolute;z-index:300;" use:floatingContent
 | 
				
			||||||
 | 
						     use:clickOutside on:outclick="{closeUserSettings}">
 | 
				
			||||||
 | 
						    <UserSettings on:signIn="{closeUserSettings}"
 | 
				
			||||||
 | 
								  on:signIn="{() => showSignIn = true}"
 | 
				
			||||||
 | 
								  on:signOut="{signOut}" />
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {#if showSignIn}
 | 
				
			||||||
 | 
						<Modal on:close="{() => showSignIn = false}">
 | 
				
			||||||
 | 
						    <svelte:fragment slot="header">
 | 
				
			||||||
 | 
							<h1 class="no-select" style="margin-top: 8px;">Sign In</h1>
 | 
				
			||||||
 | 
						    </svelte:fragment>
 | 
				
			||||||
 | 
						    <SignIn on:close="{() => showSignIn = false}"
 | 
				
			||||||
 | 
							    on:signIn="{signIn}" />
 | 
				
			||||||
 | 
						</Modal>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
					 @use "sass:string";
 | 
				
			||||||
 | 
					 @use "$lib/styles/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .container {
 | 
				
			||||||
 | 
					     overflow-y: var(--overflow-y, scroll);
 | 
				
			||||||
 | 
					     &:not(.mobile) {
 | 
				
			||||||
 | 
						 overflow: hidden;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .screen {
 | 
				
			||||||
 | 
					     @extend .d-flex;
 | 
				
			||||||
 | 
					     @extend .snap-x;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     position: relative;
 | 
				
			||||||
 | 
					     overflow-x: var(--overflow-x, scroll);
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 :global(.sidebar, .chat) {
 | 
				
			||||||
 | 
					     @extend .snap-point;
 | 
				
			||||||
 | 
					     flex-shrink: 0;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .chat {
 | 
				
			||||||
 | 
					     position: relative;
 | 
				
			||||||
 | 
					     z-index: 50;
 | 
				
			||||||
 | 
					     overflow-y: hidden;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .container:not(.mobile) .chat {
 | 
				
			||||||
 | 
					     flex-grow: 1;
 | 
				
			||||||
 | 
					     flex-basis: 0;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .mobile .chat {
 | 
				
			||||||
 | 
					     width: 100vw;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .sidebar-overlay, .overlay {
 | 
				
			||||||
 | 
					     pointer-events: var(--should-display);
 | 
				
			||||||
 | 
					     top: var(--header-size);
 | 
				
			||||||
 | 
					     position: absolute;
 | 
				
			||||||
 | 
					     width: 100%;
 | 
				
			||||||
 | 
					     height: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     background-color: rgba(0,0,0, 0.6);
 | 
				
			||||||
 | 
					     z-index: 101;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     filter: string.unquote("opacity(max(var(--opacity, 0), 0))");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     /* will-change: filter; */
 | 
				
			||||||
 | 
					     transition: {
 | 
				
			||||||
 | 
						 property: filter;
 | 
				
			||||||
 | 
						 duration: 300;
 | 
				
			||||||
 | 
						 timing-function: cubic-bezier(0.230, 1.000, 0.320, 1.000); /* easeOutQuint */
 | 
				
			||||||
 | 
					     };
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .header {
 | 
				
			||||||
 | 
					     display: contents;
 | 
				
			||||||
 | 
					     position: relative;
 | 
				
			||||||
 | 
					     & > * {
 | 
				
			||||||
 | 
						 z-index: calc(var(--header-z) + 1);
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     & > :global(:first-child), & > :global(:last-child) {
 | 
				
			||||||
 | 
						 padding-left: 16px;
 | 
				
			||||||
 | 
						 padding-right: 16px;
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .vert-center {
 | 
				
			||||||
 | 
					     display: flex;
 | 
				
			||||||
 | 
					     align-items: center;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 .room-name {
 | 
				
			||||||
 | 
					     font-weight: 900;
 | 
				
			||||||
 | 
					     font-size: 1.2em;
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/routes/+layout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/+layout.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					import type { LayoutLoad } from './$types';
 | 
				
			||||||
 | 
					import { credentials } from '$lib/chat';
 | 
				
			||||||
 | 
					export const load: LayoutLoad = async () => {
 | 
				
			||||||
 | 
					  credentials.subscribe((creds) => localStorage.setItem('chatToken', JSON.stringify(creds)));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ssr = false;
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/routes/[room]/+page.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/routes/[room]/+page.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export const ssr = false;
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/routes/[room]/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/routes/[room]/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					 import type { PageData } from './$types';
 | 
				
			||||||
 | 
					 import { page } from '$app/stores';
 | 
				
			||||||
 | 
					 import ChatMessages from "$lib/ChatMessages.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 import { room } from '$lib/chat';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 $: $room = $page.params.room.toLowerCase();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<ChatMessages />
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								static/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/favicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								static/troll_haz2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/troll_haz2.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 22 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								static/tweet.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/tweet.mp3
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										26
									
								
								svelte.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								svelte.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import adapter from '@sveltejs/adapter-auto';
 | 
				
			||||||
 | 
					import { vitePreprocess } from '@sveltejs/kit/vite';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { readFileSync } from 'fs';
 | 
				
			||||||
 | 
					import { fileURLToPath } from 'url';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const file = fileURLToPath(new URL('package.json', import.meta.url));
 | 
				
			||||||
 | 
					const json = readFileSync(file, 'utf8');
 | 
				
			||||||
 | 
					const pkg = JSON.parse(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @type {import('@sveltejs/kit').Config} */
 | 
				
			||||||
 | 
					const config = {
 | 
				
			||||||
 | 
					  // Consult https://kit.svelte.dev/docs/integrations#preprocessors
 | 
				
			||||||
 | 
					  // for more information about preprocessors
 | 
				
			||||||
 | 
					  preprocess: vitePreprocess(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  kit: {
 | 
				
			||||||
 | 
					    adapter: adapter(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    version: {
 | 
				
			||||||
 | 
					      name: pkg.version
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default config;
 | 
				
			||||||
							
								
								
									
										17
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"extends": "./.svelte-kit/tsconfig.json",
 | 
				
			||||||
 | 
						"compilerOptions": {
 | 
				
			||||||
 | 
							"allowJs": true,
 | 
				
			||||||
 | 
							"checkJs": true,
 | 
				
			||||||
 | 
							"esModuleInterop": true,
 | 
				
			||||||
 | 
							"forceConsistentCasingInFileNames": true,
 | 
				
			||||||
 | 
							"resolveJsonModule": true,
 | 
				
			||||||
 | 
							"skipLibCheck": true,
 | 
				
			||||||
 | 
							"sourceMap": true,
 | 
				
			||||||
 | 
							"strict": true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
 | 
				
			||||||
 | 
						// from the referenced tsconfig.json - TypeScript does not merge them in
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					import { sveltekit } from '@sveltejs/kit/vite';
 | 
				
			||||||
 | 
					import type { UserConfig } from 'vite';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { execSync } from 'child_process';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 | 
				
			||||||
 | 
					// @ts-ignore
 | 
				
			||||||
 | 
					void !(function () {
 | 
				
			||||||
 | 
					  typeof self == 'undefined' &&
 | 
				
			||||||
 | 
					    typeof global == 'object' &&
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    (global.self = global);
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const config: UserConfig = {
 | 
				
			||||||
 | 
					  plugins: [sveltekit()],
 | 
				
			||||||
 | 
					  resolve: {
 | 
				
			||||||
 | 
					    alias: {
 | 
				
			||||||
 | 
					      'socket.io-client': 'socket.io-client/dist/socket.io.js'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  define: {
 | 
				
			||||||
 | 
					    __COMMIT__: JSON.stringify(execSync('git rev-parse HEAD').toString().trim().slice(0, 8))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default config;
 | 
				
			||||||
		Reference in New Issue
	
	Block a user